2020前端面试汇总

2020前端面试 - JavaScript1.0篇

2020-06-13  本文已影响0人  西巴撸

前言:

2020年是多灾多难的一年,疫情持续至今,到目前,全世界的经济都受到不同程序的影响,各大公司裁员,在这样一片严峻的形式下,找工作更是难上加难。

企业的门槛提高,第一,对于学历的要求,必须学信网可查的统招本科;第二,对于技术的掌握程序,更多的是底层原理,项目经验,等等。

下面是面试几周以来,总结的一些面试中常被问到的题目,还有吸取的一些前辈们分享的贴子,全部系统的罗列出来,希望能够帮到正在面试的人。

JavaScript原理和底层是最重要,也是最常问的,包括ES6。

1. 基础类型和类型检测
1. 判断基本类型
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"

2. 判断原生引用类型
**函数类型**
Function fn(){
  console.log(“test”);
}
Object.prototype.toString.call(fn); // "[object Function]"
**日期类型**
var date = new Date();
Object.prototype.toString.call(date); // "[object Date]"
**数组类型**
var arr = [1,2,3];
Object.prototype.toString.call(arr); // "[object Array]"
**正则表达式**
var reg = /[hbc]at/gi;
Object.prototype.toString.call(reg); // "[object RegExp]"
**自定义类型**
function Person(name, age) {
    this.name = name;
    this.age = age;
}
var person = new Person("Rose", 18);
Object.prototype.toString.call(arr); // "[object Object]"
很明显这种方法不能准确判断person是Person类的实例,而只能用instanceof 操作符来进行判断,如下所示:
console.log(person instanceof Person); // true

3. 判断原生JSON对象
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON);
console.log(isNativeJSON);// 输出结果为”[object JSON]”说明JSON是原生的,否则不是;
2. 类型转换
3. == 与 ===
4. valueOf 和 toString
5. 原型和原型链
6. Class和继承
7. 闭包
8. this
9. 修改this指向的几种方法
10. new的原理
11. 实现一个new函数
let _new = function(factory, ...rest) {
    let o = {
      "__proto__": factory.prototype
    }
    let res = factory.apply(o, rest)
    return typeof res === 'object' ? res : o;
}
12. let、const、var
13. 箭头函数
14. Promise详解
const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
promise.then(()=>{
  // ...
}).then(()=> {
  
});

2.Promise.prototype.catch()
Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

promise.then(()=>{
  // ...
}).catch((error)=> {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

下面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

3.Promise.prototype.finally()
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。【ES2018】

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
finally本质上是then方法的特例。

4.Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

5.Promise.race()
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race()是异步操作竞赛,值返回最快的一个

6.Promise.allSettled()
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。【ES2020】

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。

7.Promise.any()
Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

Promise.any()Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束。

const promises = [
  fetch('/endpoint-a').then(() => 'a'),
  fetch('/endpoint-b').then(() => 'b'),
  fetch('/endpoint-c').then(() => 'c'),
];
try {
  const first = await Promise.any(promises);
  console.log(first);
} catch (error) {
  console.log(error);
}

上面代码中,Promise.any()方法的参数数组包含三个 Promise 操作。其中只要有一个变成fulfilledPromise.any()返回的 Promise 对象就变成fulfilled。如果所有三个操作都变成rejected,那么await命令就会抛出错误。

8.Promise.resolve()
有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

const jsPromise = Promise.resolve($.ajax('/whatever.json'));

上面代码将 jQuery 生成的deferred对象,转为一个新的 Promise 对象。

Promise.resolve()等价于下面的写法。

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

9.Promise.reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。

注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

10.Promise.try()

让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API ,简言之就是,让同步操作也可以像异步一样执行。

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
15. ES6新特性
16. CommonJs
17. CommonJS和ES6模块的区别?
18. 什么是UMD?AMD和CMD?
19. 浮点数计算精确度问题
0.1 + 0.2 = 0.30000000000000004
1.0 - 0.9 = 0.09999999999999998

2.将数扩大至整数,再进行计算

3.使用例如number-precision等第三库进行计算

20. 最大安全数
21. Array对象
// bad
var arr = new Array(1, 2);

// good  字面量法,更好的声明方式
var arr = [1, 2];
var arr = [1, 2, 3];

typeof arr // "object"
Array.isArray(arr) // true

以下都是实例方法

var arr = [1, 2, 3];
arr.valueOf() // [1, 2, 3]
var arr = [1, 2, 3];
arr.toString() // "1,2,3"

var arr = [1, 2, 3, [4, 5, 6]];
arr.toString() // "1,2,3,4,5,6"
var arr = [];

arr.push(1) // 1
arr.push('a') // 2
arr.push(true, {}) // 4
arr // [1, 'a', true, {}]
var arr = ['a', 'b', 'c'];

arr.pop() // 'c'
arr // ['a', 'b']

// 对空数组使用pop方法,不会报错,而是返回undefined。
[].pop() // undefined

// push()和pop()结合使用,就构成了“后进先出”的栈结构(stack)。
var arr = [];
arr.push(1, 2);
arr.push(3);
arr.pop();
arr // [1, 2]

// 上面代码中,3是最后进入数组的,但是最早离开数组。
var a = ['a', 'b', 'c'];

a.shift() // 'a'
a // ['b', 'c']

// shift方法可以遍历并清空一个数组。
var list = [1, 2, 3, 4, 5, 6];
var item;

while (item = list.shift()) {
  console.log(item);
}

list // []

push和shift结合使用,就构成了“先进先出”的队列结构(queue)。
var a = ['a', 'b', 'c'];

a.unshift('x'); // 4
a // ['x', 'a', 'b', 'c']

// unshift方法可以接受多个参数,这些参数都会添加到目标数组头部。
var arr = [ 'c', 'd' ];
arr.unshift('a', 'b') // 4
arr // [ 'a', 'b', 'c', 'd' ]
var a = [1, 2, 3, 4];

a.join(' ') // '1 2 3 4'
a.join(' | ') // "1 | 2 | 3 | 4"
a.join() // "1,2,3,4"

// 如果数组成员是undefined或null或空位,会被转成空字符串
[undefined, null].join('#')
// '#'

['a',, 'b'].join('-')
// 'a--b'

// 通过call方法,这个方法也可以用于字符串或类似数组的对象。
Array.prototype.join.call('hello', '-')
// "h-e-l-l-o"

var obj = { 0: 'a', 1: 'b', length: 2 };
Array.prototype.join.call(obj, '-')
// 'a-b'
['hello'].concat(['world'])
// ["hello", "world"]

['hello'].concat(['world'], ['!'])
// ["hello", "world", "!"]

[].concat({a: 1}, {b: 2})
// [{ a: 1 }, { b: 2 }]

[2].concat({a: 1})
// [2, {a: 1}]

// 除了数组作为参数,concat也接受其他类型的值作为参数,添加到目标数组尾部
[1, 2, 3].concat(4, 5, 6)
// [1, 2, 3, 4, 5, 6]

// 如果数组成员包括对象,concat方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用
var obj = { a: 1 };
var oldArray = [obj];

var newArray = oldArray.concat();

obj.a = 2;
newArray[0].a // 2

上面代码中,原数组包含一个对象,concat方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变。
var a = ['a', 'b', 'c'];

a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]
var a = ['a', 'b', 'c'];
a.slice(0) // ["a", "b", "c"]
a.slice(1) // ["b", "c"]
a.slice(1, 2) // ["b"]
a.slice(2, 6) // ["c"]
a.slice() // ["a", "b", "c"]   最后一个例子没有参数,实际上等于返回一个原数组的拷贝


// 如果slice方法的参数是负数,则表示倒数计算的位置
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]    -2表示倒数计算的第二个位置,-1表示倒数计算的第一个位置


// 如果第一个参数大于等于数组长度,或者第二个参数小于第一个参数,则返回空数组
var a = ['a', 'b', 'c'];
a.slice(4) // []
a.slice(2, 1) // []

slice方法的一个重要应用,是将类似数组的对象转为真正的数组

Array.prototype.slice.call({
  0: 'a',
  1: 'b',
  length: 2
})
// ['a', 'b']

Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);

上面代码的参数都不是数组,但是通过call方法,在它们上面调用slice方法,就可以把它们转为真正的数组。

// 第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素
arr.splice(start, count, addElement1, addElement2, ...);


// 从原数组4号位置,删除了两个数组成员
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2) // ["e", "f"]
a // ["a", "b", "c", "d"]


// 除了删除成员,还插入了两个新成员
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]


// 起始位置如果是负数,就表示从倒数位置开始删除
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(-4, 2) // ["c", "d"]  倒数第四个位置c开始删除两个成员


// 如果只是单纯地插入元素,splice方法的第二个参数可以设为0。
var a = [1, 1, 1];
a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]


// 如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组。
var a = [1, 2, 3, 4];
a.splice(2) // [3, 4]
a // [1, 2]
['d', 'c', 'b', 'a'].sort()
// ['a', 'b', 'c', 'd']

[4, 3, 2, 1].sort()
// [1, 2, 3, 4]

[11, 101].sort()
// [101, 11]

[10111, 1101, 111].sort()
// [10111, 1101, 111]

上面代码的最后两个例子,需要特殊注意。sort方法不是按照大小排序,而是按照字典顺序。也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以101排在11的前面。

// 如果想让sort方法按照自定义方式排序,可以传入一个函数作为参数
[10111, 1101, 111].sort(function (a, b) {
  return a - b;
})
// [111, 1101, 10111]

上面代码中,sort的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于0,表示第一个成员排在第二个成员后面;其他情况下,都是第一个元素排在第二个元素前面。

[
  { name: "张三", age: 30 },
  { name: "李四", age: 24 },
  { name: "王五", age: 28  }
].sort(function (o1, o2) {
  return o1.age - o2.age;
})
// [
//   { name: "李四", age: 24 },
//   { name: "王五", age: 28  },
//   { name: "张三", age: 30 }
// ]
var numbers = [1, 2, 3];

numbers.map(function (n) {
  return n + 1;
});
// [2, 3, 4]

numbers
// [1, 2, 3]

上面代码中,numbers数组的所有成员依次执行参数函数,运行结果组成一个新数组返回,原数组没有变化。

map方法接受一个函数作为参数。该函数调用时,map方法向它传入三个参数:当前成员、当前位置和数组本身。

[1, 2, 3].map(function(elem, index, arr) {
  return elem * index;
});
// [0, 2, 6]

上面代码中,map方法的回调函数有三个参数,elem为当前成员的值,index 为当前成员的位置,arr 为原数组([1, 2, 3])。

map方法还可以接受第二个参数,用来绑定回调函数内部的this变量。

var arr = ['a', 'b', 'c'];

[1, 2].map(function (e) {
  return this[e];
}, arr)
// ['b', 'c']

上面代码通过map方法的第二个参数,将回调函数内部的this对象,指向arr数组。

如果数组有空位,map方法的回调函数在这个位置不会执行,会跳过数组的空位。

var f = function (n) { return 'a' };

[1, undefined, 2].map(f) // ["a", "a", "a"]
[1, null, 2].map(f) // ["a", "a", "a"]
[1, , 2].map(f) // ["a", , "a"]

上面代码中,map方法不会跳过undefinednull,但是会跳过空位。

function log(element, index, array) {
  console.log('[' + index + '] = ' + element);
}

[2, 5, 9].forEach(log);
// [0] = 2
// [1] = 5
// [2] = 9

上面代码中,forEach遍历数组不是为了得到返回值,而是为了在屏幕输出内容,所以不必使用map方法。

forEach方法也可以接受第二个参数,绑定参数函数的this变量。

var out = [];

[1, 2, 3].forEach(function(elem) {
  this.push(elem * elem);
}, out);

out // [1, 4, 9]

上面代码中,空数组outforEach方法的第二个参数,结果,回调函数内部的this关键字就指向out

注意,forEach方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时,就中断遍历,要使用for循环。

var arr = [1, 2, 3];

for (var i = 0; i < arr.length; i++) {
  if (arr[i] === 2) break;
  console.log(arr[i]);
}
// 1

上面代码中,执行到数组的第二个成员时,就会中断执行。forEach方法做不到这一点。

forEach方法也会跳过数组的空位。

var log = function (n) {
  console.log(n + 1);
};

[1, undefined, 2].forEach(log)
// 2
// NaN
// 3

[1, null, 2].forEach(log)
// 2
// 1
// 3

[1, , 2].forEach(log)
// 2
// 3

上面代码中,forEach方法不会跳过undefinednull,但会跳过空位。

[1, 2, 3, 4, 5].filter(function (elem) {
  return (elem > 3);
})
// [4, 5]  上面代码将大于3的数组成员,作为一个新数组返回。

var arr = [0, 1, 'a', false];

arr.filter(Boolean)
// [1, "a"]  上面代码中,filter方法返回数组arr里面所有布尔值为true的成员。

filter方法的参数函数可以接受三个参数:当前成员,当前位置和整个数组

[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
  return index % 2 === 0;
});
// [1, 3, 5]  上面代码返回偶数位置的成员组成的新数组。

filter方法还可以接受第二个参数,用来绑定参数函数内部的this变量。

var obj = { MAX: 3 };
var myFilter = function (item) {
  if (item > this.MAX) return true;
};

var arr = [2, 8, 3, 4, 1, 3, 2, 9];
arr.filter(myFilter, obj) // [8, 4, 9]

上面代码中,过滤器myFilter内部有this变量,它可以被filter方法的第二个参数obj绑定,返回大于3的成员。

some方法是只要一个成员的返回值是true,则整个some方法的返回值就是true,否则返回false。(一真即真)

var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
  return elem >= 3;
});
// true  上面代码中,如果数组arr有一个成员大于等于3,some方法就返回true。

every方法是所有成员的返回值都是true,整个every方法才返回true,否则返回false。(一假即假)

var arr = [1, 2, 3, 4, 5];
arr.every(function (elem, index, arr) {
  return elem >= 3;
});
// false  上面代码中,数组arr并非所有成员大于等于3,所以返回false。

注意,对于空数组,some方法返回falseevery方法返回true,回调函数都不会执行。

function isEven(x) { return x % 2 === 0 }

[].some(isEven) // false
[].every(isEven) // true

someevery方法还可以接受第二个参数,用来绑定参数函数内部的`this变量。

[1, 2, 3, 4, 5].reduce(function (a, b) {
  console.log(a, b);
  return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
//最后结果:15

上面代码中,reduce方法求出数组所有成员的和。第一次执行,a是数组的第一个成员1,b是数组的第二个成员2。第二次执行,a为上一轮的返回值3,b为第三个成员3。第三次执行,a为上一轮的返回值6,b为第四个成员4。第四次执行,a为上一轮返回值10,b为第五个成员5。至此所有成员遍历完成,整个方法的返回值就是最后一轮的返回值15。

reduce方法和reduceRight方法的第一个参数都是一个函数。该函数接受以下四个参数。
1.累积变量,默认为数组的第一个成员
2.当前变量,默认为数组的第二个成员
3.当前位置(从0开始)
4.原数组
这四个参数之中,只有前两个是必须的,后两个则是可选的。

如果要对累积变量指定初值,可以把它放在reduce方法和reduceRight方法的第二个参数。

[1, 2, 3, 4, 5].reduce(function (a, b) {
  return a + b;
}, 10);
// 25

上面代码指定参数a的初值为10,所以数组从10开始累加,最终结果为25。注意,这时b是从数组的第一个成员开始遍历。
上面的第二个参数相当于设定了默认值,处理空数组时尤其有用。

function add(prev, cur) {
  return prev + cur;
}

[].reduce(add)
// TypeError: Reduce of empty array with no initial value
[].reduce(add, 1)
// 1

上面代码中,由于空数组取不到初始值,reduce方法会报错。这时,加上第二个参数,就能保证总是会返回一个值。

下面是一个reduceRight方法的例子。

function substract(prev, cur) {
  return prev - cur;
}

[3, 2, 1].reduce(substract) // 0
[3, 2, 1].reduceRight(substract) // -4

上面代码中,reduce方法相当于3减去2再减去1,reduceRight方法相当于1减去2再减去3。

由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员。

function findLongest(entries) {
  return entries.reduce(function (longest, entry) {
    return entry.length > longest.length ? entry : longest;
  }, '');
}

findLongest(['aaa', 'bb', 'c']) // "aaa"

上面代码中,reduce的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员。

indexOf方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回-1

var a = ['a', 'b', 'c'];

a.indexOf('b') // 1
a.indexOf('y') // -1

indexOf方法还可以接受第二个参数,表示搜索的开始位置。

['a', 'b', 'c'].indexOf('a', 1) // -1
// 上面代码从1号位置开始搜索字符a,结果为`-1`,表示没有搜索到。

lastIndexOf方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1

var a = [2, 5, 9, 2];
a.lastIndexOf(2) // 3
a.lastIndexOf(7) // -1

注意,这两个方法不能用来搜索NaN的位置,即它们无法确定数组成员是否包含NaN

[NaN].indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1

这是因为这两个方法内部,使用严格相等运算符(===)进行比较,而NaN是唯一一个不等于自身的值。

var users = [
  {name: 'tom', email: 'tom@example.com'},
  {name: 'peter', email: 'peter@example.com'}
];

users
.map(function (user) {
  return user.email;
})
.filter(function (email) {
  return /^t/.test(email);
})
.forEach(console.log);
// "tom@example.com"

上面代码中,先产生一个所有 Email 地址组成的数组,然后再过滤出以t开头的 Email 地址。

22. 数组的扁平化处理(降维)
[1, [2], [[3]]] => [1,2,3]

1.使用Array.prototype.flat(depth)。depth不能小于数组的深度

arr.flat(3)

2.遍历

function flat1(arr) {
  while(arr.some(item=>Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}

3.递归实现

function flat2(arr1) {
  return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flat2(val)) : acc.concat(val), []);
}

4.非递归

function stackFlatten(input) {
  const stack = [...input];
  const res = [];
  while (stack.length) {
    const next = stack.pop();
    if (Array.isArray(next)) {
      stack.push(...next);
    } else {
      res.push(next);
    }
  }
  return res.reverse();
}
{
    num: 1,
    arr: [1, 2]
    obj: {
        name: 'name'
    }
}

// 偏平化后=>
{
    num: 1,
    arr.[0]: 1,
    arr.[1]: 2,
    obj.name: 'name'
}
23. 深拷贝浅拷贝

首先:浅拷贝和深拷贝都只针对于Object, Array这样的复杂对象。
区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制

24. 防抖和节流
function debounce(fn, delay) {
  let timer;
  return function(...rest) {
    timer && clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, rest), delay)
  }
}
function throttle(fn, delay) {
  let start
  return function(...rest) {
    let now =  Date.now()
    !start && (start = now)

    if (now - start >= delay) {
      fn.apply(this, rest)
      start = now
    }
  }
}

function throttle2(fn, delay){
  let timer
  return function(...rest){
    if(!timer){
      timer = setTimeout(() => {
        fn.apply(this, rest);
        timer = null;
      }, delay)
    }
  }
}
25. 原生ajax
var xhr=new XMLHttpRequest();
xhr.open('POST',url,false);
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.onreadystatechange=function(){
    // readyState == 4说明请求已完成
    if(xhr.readyState==4){
        if(xhr.status==200 || xhr.status==304){
            console.log(xhr.responseText);
            fn.call(xhr.responseText);
        }
    }
}
xhr.send();
26. 函数柯里化
function curry(fn, ...args) {
  return args.length < fn.length ? (...arguments) => curry(fn, ...args, ...arguments) : fn(...args)
}
27. map和forEach的区别
28. Cookie和Session的区别
29. 事件捕获流,冒泡流和事件委托
//阻止冒泡
e.stopPropagation() || return false。
window.e.cancelBubble=true; // IE

// 阻止捕获
e.stopImmediatePropagation() // 阻止捕获和其他事件

// 阻止默认事件: 事件处理过程中,不阻击事件冒泡,但阻击默认行为
e.preventDefault()
window.e.returnValue=false; || return false;// IE
30. 作用域、作用域链
31. 数组去重
<script>
    // 这里要把方法添加到原型上,这样实例对象可以共享
    // 因为实例对象不能调静态方法,构造函数才可以调静态方法
    // indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。如果有则返回对象元素的索引,如果没有则返回-1
    var arr = [1, 2, 2, 3, 4, 5, 3, 4, 5];

    // 方法1
    Array.prototype.only1 = function () {
        var customArr = [];
        for (var i = 0; i < this.length; i++) {
            // 如果当前数组的第i已经保存进了临时数组,那么跳过,
            // 否则把当前项push到临时数组里面
            if (customArr.indexOf(this[i]) == -1) customArr.push(this[i]);
        }
        return customArr;
    }
    console.log(arr.only1());


    // 方法2  把数组里的元素作为对象的key来判断 也称为哈希表
    Array.prototype.only2 = function () {
        // hash表  自定义一个空数组
        var obj = {}, customArr = [];
        // 遍历数组
        for (var i = 0; i < this.length; i++) {
            if (!obj[this[i]]) {
                obj[this[i]] = true;  // 存入hash表中
                customArr.push(this[i]);  // 把当前数组的当前项添加到自定义数组里面
            }
        }
        return customArr;
    }
    console.log(arr.only2());


    // 方法3
    Array.prototype.only3 = function () {
        var custom = [this[0]];  // 结果数组
        for (var i = 1; i < this.length; i++) {  // 从第二项开始遍历
            // 如果当前数组的第i项在当前数组中第一次出现的位置不是i,
            // 那么表示第i项是重复的,忽略掉。否则存入结果数组
            if (this.indexOf(this[i]) == i) custom.push(this[i]);
        }
        return custom;
    }
    console.log(arr.only3());
</script>

其中第1种和第3种方法都用到了数组的indexOf方法。此方法的目的是寻找存入参数在数组中第一次出现的位置。很显然,js引擎在实现这个方法的时候会遍历数组直到找到目标为止。所以此函数会浪费掉很多时间。

而第2中方法用的是hash表。把已经出现过的通过下标的形式存入一个object内。下标的引用要比用indexOf搜索数组快的多。

为了判断这三种方法的效率如何,我做了一个测试程序,生成一个10000长度的随机数组来去重测试执行时间。 结果表明第二种方法远远快于其他两种方法。 但是内存占用方面应该第二种方法比较多,因为多了一个hash表。**这就是所谓的空间换时间。 **

Array.prototype.only4= function()
{
    this.sort();
    var re=[this[0]];
    for(var i = 1; i < this.length; i++)
    {
        if( this[i] !== re[re.length-1])
        {
            re.push(this[i]);
        }
    }
    return re;
}
console.log(arr.only3());

这个方法的思路是先把数组排序,然后比较相邻的两个值。 排序的时候用的JS原生的sort方法,JS引擎内部应该是用的快速排序吧。 最终测试的结果是此方法运行时间平均是第二种方法的三倍左右,不过比第一种和第三种方法快了不少。

然而当你提到排序的时候,面试官就会继续追问你,除了sort(),你还会那种排序。显然这是一个无底洞,当你回答上来桶排序,冒泡排序,希尔排序的时候,他会继续问你,你对那种比较熟悉,现在能写的出来么?

下面是十大排序

**十大经典排序**

冒泡排序最为稳定 但是几乎没有什么卵用

<script>
    function bubbleSort(arr) {
        var len = arr.length;
        for (var i = 0; i < len; i++) {
            for (var j = 0; j < len - 1; j++) {
                if (arr[j] > arr[j + 1]) {   // 相邻两元素进行比较
                    var temp = arr[j + 1];   // 元素交换
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        return arr;
    }
    var sortArr = [1, 23, 43, 5, 42, 4, 32, 53, 64, 6];
    console.log(bubbleSort(sortArr));;
</script>
上一篇 下一篇

猜你喜欢

热点阅读