面经总结-JS

面经总结系列

Posted by BY Bamboo on December 7, 2020

ES6 模块与 CommonJS 的区别

  • CommonJS 模块输出的是一个值的拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。运行时加载;
  • ES6 模块输出的是值的引用,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化。静态加载

继承

  • 组合继承:优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了两次父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
  • 拷贝继承 : 属性通过 : 父类.call(this) 方法通过 : 对象拷贝 ( for in , Object.assign(子类 , 父类) , … )

  • 类式继承 (寄生组合式继承): 属性继承,也是用 call 的方式来实现。 方法的继承,空的 F 函数,new F()实现的。

var F = function(){};
F.prototype = Foo.prototype;
Bar.prototype = new F();
Bar.prototype.constructor = Bar;
  • Class 继承:使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super
class Child extends Parent {
  constructor(value) {
    super(value) 让this指向子类。super必须要写在构造器的第一行
    this.val = value
  }
}

原型和原型链

  • 每个函数都有 prototype 属性,该属性指向其原型,原型的 constructor 属性又指向构造函数
  • 每个对象 (除了 null)都有proto指向创建该对象的构造函数的原型
  • 原型链就是多个对象通过proto的方式连接了起来
    当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype。 image
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
当获取 person.constructor 时,其实 person 中并没有 constructor 属性, 当不能读取到 constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
person.constructor === Person.prototype.constructor

闭包?

  • 闭包就是可以访问其他函数内部变量的函数,本质上说就是在函数内部和函数外部搭建起一座桥梁,使得子函数可以访问父函数中所有的局部变量,但是反之不可以,这只是闭包的作用之一,另一个作用,则是保护变量不受外界污染,使其一直存在内存中,在工作中我们还是少使用闭包的好,因为闭包太消耗内存,不到万不得已的时候尽量不使用。

数组方法

  • 不能修改原数组的方法:join、concat、slice、map、filter
  • 可以修改原数组的方法:push、pop、unshift、shift、splice、reverse、sort
  • var b = a.concat(); var b = a.slice();自带的 concat 和 slice 默认是浅拷贝
  • sort 使用的是插入排序和快速排序结合的排序算法。数组长度不超过 10 时,使用插入排序。长度超过 10 使用快速排序。

ES6

1、let const

  • var 命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined。
  • let 所声明的变量一定要在声明后使用,否则报错。
  • 只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
  • 在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”。
  • let 不允许在相同作用域内,重复声明同一个变量。
  • const 声明一个只读的常量。一旦声明,常量的值就不能改变。

2、解构赋值

let [a, b, c] = [1, 2, 3];
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

3、模板字符串
4、新的原始数据类型 Symbol,表示独一无二的值。第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

let sy = Symbol('a')
let sy1 = Symbol('a')
sy === sy1  // false
sy.toString() === sy1.toString() // true
// ES2019 提供 description
sy.description === sy1.description // true

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true

Symbol.for()与 Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的 key 是否已经存在,如果不存在才会新建一个值。比如,如果你调用 Symbol.for(“foo”)30 次,每次都会返回同一个 Symbol 值,但是调用 Symbol(“foo”)30 次,会返回 30 个不同的 Symbol 值。

5、Set

  • 类似于数组的数据结构,成员值都是唯一且没有重复的值,Set 内的元素是强类型,会进行类型检查。
// 定义
const set = new Set(arr)
// 数组去重
[...new Set(arr)]

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]); // Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3}
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x))); // Set {1}

6、WeakSet

  • 和 Set 结构类似,成员值只能是对象
  • 成员都是弱引用,垃圾回收机制不考虑 WeakSet 结构对此成员的引用
  • 成员不适合引用,它会随时消失,因此 ES6 规定 WeakSet 结构不可遍历
  • 其他对象不再引用成员时,垃圾回收机制会自动回收此成员所占用的内存,不考虑此成员是否还存在于 WeakSet 结构中

7、Map

  • 类似于对象的数据结构,但是 “键” 的范围不限于字符串,各种类型的值(包括对象)都可以当作键
  • WeakMap 成员键只能是对象,其他特点同 WeakSet

new 一个构造函数发生了什么

function create(func, args) {
  let obj = {}; //创建一个新对象;
  obj.__proto__ = func.prototype; //设置新对象的_proto_属性指向构造函数的prototype
  let result = func.call(obj, ...args); //让构造函数的this指向新对象,并执行构造函数(为新对象添加属性)
  return result instanceof Object ? result : obj; //确保返回值为对象
}

instanceof

  • 通过判断对象的原型链中是不是能找到类型的 prototype
function myInstanceof(left, right) {
  let prototype = right.prototype; //类型的 prototype
  left = left.__proto__; //对象的原型
  while (true) {
    if (left === null || left === undefined)
      //直到对象原型为 null,因为原型链最终为 null
      return false;
    if (prototype === left) return true;
    left = left.__proto__;
  }
}

数组扁平化

let arr = [1, [2, [3, 4]]];
function foo(arr) {
  1 return arr.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? foo(cur) : cur);
    }, []);

  2 return arr
    .toString()
    .split(",")
    .map(item => {
      return +item;
    });

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

  4 return arr.flat(Infinity)
}
console.log(foo(arr));

防抖函数

function antiShake(fn , delay){
    var timer;
    return function(){
        clearTimeout(timer);
        timer = setTimeout(function(){
            fn();
        },delay);
    }
}

节流函数

function throttle( fn , delay ){
    var flag = true;
    return function(){
        if(!flag){
            return;
        }
        flag = false;
        setTimeout(function(){
            fn();
            flag = true;
        },delay);
    };
}

深拷贝

  • JSON.parse(JSON.stringify(object)) 有局限性的 会忽略 undefined 会忽略 symbol
function isArray(arr) {
  return Object.prototype.toString.call(arr) === "[object Array]";
}

function deepCopy(obj) {
  let result;
  if (isArray(obj)) {
    result = [];
  } else {
    result = {};
  }
  for (let attr in obj) {
    if (typeof obj[attr] === "object") {
      result[attr] = arguments.callee(obj[attr]);
    } else {
      result[attr] = obj[attr];
    }
  }
  return result;
}

call bind apply 有什么区别?

  • call()和 apply()的区别就在于,两者之间的参数。
  • call() 和 apply()的第一个参数相同,就是指定的对象。这个对象就是该函数的执行上下文。
  • call()在第一个参数之后的 后续所有参数就是传入该函数的值。
  • apply() 只有两个参数,第一个是对象,第二个是数组,这个数组就是该函数的参数。
  • bind() 方法和前两者不同在于: bind() 方法会返回执行上下文被改变的函数而不会立即执行,而前两者是直接执行该函数。他的参数和 call()相同。

实现 bind

Function.prototype.myBind = function(context, ...arr1) {
  if (typeof this !== "function") {
    throw new TypeError("Bind must be called on a function");
  }
  let This = this;
  context = context || window;

  return function(...arr2) {
    let arr = arr1.concat(arr2);
    // 因为返回了一个函数,我们可以 new,new 的优先级高于 bind
    if (this instanceof This) {
      return new This(...arr);
    }
    This.call(context, ...arr);
  };
};

function foo(n1, n2, n3, n4) {
  console.log(this);
  console.log(n1, n2, n3, n4);
}

foo.myBind(document, 3)(4, 5, 6);

call

Function.prototype.call = function(context, ...args) {
  context = context || window;
  context.func = this;

  let res = context.func(...args); //getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  delete context.func;
  return res;
};

jsonP

  • 如果服务端接口到 get 请求,返回的是 bar({message:’hello’}),这样的话在服务端不就可以把数据通过函数执行传参的方式实现数据传递了吗。
function jsonP(url, cbName, cb) {
  let script = document.createElement("script");
  script.src = url;
  window[cbName] = cb;
  document.body.appendChild(script);
}
jsonP("http://www.b.com/20190902/jsonp.php?callback=bar", "bar", function(
  data
) {
  console.log(data);
});

reduce

Array.prototype.myReduce = function(fn, pre) {
  for (let i = 0; i < this.length; i++) {
    if (typeof pre === "undefined") {
      pre = this[i];
    } else {
      pre = fn(pre, this[i], i, this);
    }
  }
  return pre;
};

map

Array.prototype.myMap = function(fn) {
  let arr = [];
  for (let i = 0; i < this.length; i++) {
    arr.push(fn(this[i], i, this));
  }
  return arr;
};

题目

var arr = new Array(3);
arr[0] = 2;
var re = arr.map(function(el) {
  return "1";
});
console.log(re); //[ '1', <2 empty items> ]
----------------------------------------------
function test(person) {
  person.age = 26;
  person = {
    name: "yyy",
    age: 30
  };
  return person;
}
const p1 = {
  name: "yck",
  age: 25
};
const p2 = test(p1);
console.log(p1); //{ name: 'yck', age: 26 }
console.log(p2); // { name: 'yyy', age: 30 }
------------------------------------------------
var arr = [1, 2, 3];
console.log(arr instanceof Array); //true
-----------------------------------------------
console.log("a" + +"b"); //aNaN
console.log(Number([])); //0
console.log(Number([1])); //1
console.log(Number([1, 2])); //NaN
-----------------------------------------------
function A() {

}

function B(a) {
    this.a = a
}
A.prototype.a = 1
B.prototype.b = 1
console.log(new A()); //A {}
console.log(new B()) //B { a: undefined }
console.log(new B(2)); //B { a: 2 }
console.log(B(2)); //undefined

封装 Ajax

function myAjax(opt) {
  let xhr = new XMLHttpRequest();
  if (opt.method.toLowerCase() === "get") {
    xhr.open("get", opt.url + "?" + formatData(opt.data));
    xhr.send(null);
  } else if (opt.method.toLowerCase() === "post") {
    xhr.open("post", opt.url);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xhr.send(formatData(opt.data));
  }
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      opt.success(xhr.responseText);
    } else {
      opt.error(xhr.statusText);
    }
  };
}

function formatData(obj) {
  let res = [];
  for (let attr in obj) {
    res.push(attr + "=" + obj[attr]);
  }
  return res.join("&");
}

实现 promise

class MyPromise {
  constructor(fn) {
    this.resolveArr = [];
    this.rejectArr = [];
    this.state = "pending";

    let resolveFn = () => {
      if (this.state === "pending") {
        this.state = "fulfilled";
        setTimeout(() => {
          for (let i = 0; i < this.resolveArr.length; i++) {
            this.resolveArr[i]();
          }
        }, 0);
      }
    };
    let rejectFn = () => {
      if (this.state === "pending") {
        this.state = "rejected";
        setTimeout(() => {
          for (let i = 0; i < this.rejectArr.length; i++) {
            this.rejectArr[i]();
          }
        }, 0);
      }
    };
    fn(resolveFn, rejectFn);
  }

  then(cb) {
    if (this.state === "pending") {
      this.resolveArr.push(cb);
      return this;
    }
  }

  catch(cb) {
    if (this.state === "pending") {
      this.rejectArr.push(cb);
      return this;
    }
  }
}

function foo() {
  return new MyPromise(function(resolve, reject) {
    setTimeout(() => {
      resolve();
      // reject();
    }, 1000);
  });
}

foo()
  .then(function() {
    console.log(1);
  })
  .catch(function() {
    console.log(2);
  })
  .then(function() {
    console.log(3);
  });

排序算法

  • 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
  • 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面; image
// 冒泡
function foo(arr) {
  for (let i = arr.length - 1; i >= 0; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j] > arr[j + 1]) {
        let tmp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = tmp;
      }
    }
  }
  return arr;
}

// 插入
function foo(arr) {
  for (let i = 1; i < arr.length; i++) {
    for (let j = i - 1; j >= 0; j--) {
      if (arr[j] > arr[j + 1]) {
        let tmp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = tmp;
      }
    }
  }
  return arr;
}

// 选择
function foo(arr) {
  for (let i = 0; i < arr.length; i++) {
    let min = i;
    for (let j = i + 1; j < arr.length; j++) {
      min = arr[j] < arr[min] ? j : min;
    }
    let tmp = arr[min];
    arr[min] = arr[i];
    arr[i] = tmp;
  }
  return arr;
}

// 快排
function foo(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  let middle = Math.floor(arr.length / 2);
  let left = [];
  let right = [];
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < arr[middle]) {
      left.push(arr[i]);
    } else if (arr[i] > arr[middle]) {
      right.push(arr[i]);
    }
  }
  return foo(left).concat(arr[middle], foo(right));
}

//二分查找 在有序数组中查找特定元素
function binary_search(arr, key) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] == key) {
      return mid;
    } else if (arr[mid] < key) {
      left = mid + 1;
    } else if (arr[mid] > key) {
      right = mid - 1;
    }
  }
}