js 常见手写代码题总结

165 阅读8分钟

一、实现 call(Function.prototype.call)

call核心:

  • 第一个参数是null或者undefined时,默认上下文为全局对象window
  • 接下来给context创建一个fn属性,并将值设置为需要调用的函数
  • 为了避免函数和上下文(context)的属性发上冲突,使用Symbol类型
  • 调用函数
  • 函数执行完成后删除context.fn属性
  • 返回执行结果

Function是构造函数、Function.prototype 构造函数的原型对象,这个原型对象中包含很多方法,所以模仿call就在这个原型对象上添加方法就好了

Function.prototype.call = function(context = window, ...args){
  if (typeof this !== 'function') {
    throw new TypeError('Type Error')
  }
  const fn = Symbol('fn');
  context[fn] = this;
  const res = context[fn](args);
  delete context[fn];
  return res;
}

二、实现 apply

apply核心:

  • 前面的步骤和call基本一致
  • 第二个参数可以不传,但类型必须为数组或者类数组
Function.prototype.myApply = function(context = windowm, ...args) {

   if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
   const fn = Symbol('fn');
   context[fn] = this;
   const res = context[fn](...args);
   delete context[fn];
   return res;
}

三、实现bind:

  • 对于普通函数,绑定this指向

  • 对于构造函数,要保证原函数的原型对象上的属性不能丢失

    会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

Function.prototype.bind = function(context, ...args) {
  if (typeof this !== 'function') {
    throw new Error("Type Error");
  }
  // 保存this的值
  var self = this;
  return function F() {
    // 考虑new的情况
    // 对于普通函数,绑定this指向
    // 当返回的绑定函数作为构造函数被new调用,绑定的上下文指向实例对象 
    if(this instanceof F) {
      return new self(...args, ...arguments)
    }
    return self.apply(context, [...args, ...arguments])
  }
}

总结:call apply bind的作用和区别

call、apply、bind的作用都是改变函数运行时的this指向。 bind和call、apply在使用上有所不同,bind在改变this指向的时候,返回一个改变执行上下文的函数,不会立即执行函数,而是需要调用该函数的时候再调用即可,但是call和apply在改变this指向的同时执行了该函数。 bind只接收一个参数,就是this指向的执行上文。 call、apply接收多个参数,第一个参数都是this指向的执行上文,后面的参数都是作为改变this指向的函数的参数。但是call和apply参数的格式不同,call是一个参数对应一个原函数的参数,但是apply第二个参数是数组,数组中每个元素代表函数接收的参数,数组有几个元素函数就接收几个元素。

call的应用场景:

  • 对象的继承,在子构造函数这种调用父构造函数,但是改变this指向,就可以继承父的属性 function
superClass () { 
  this.a = 1; 
  this.print = function () { console.log(this.a); } } 
  function subClass () { 

     superClass.call(this); // 执行superClass,并将superClass方法中的this指向subClass 
    this.print(); 
} 

subClass(); 
  • 借用Array原型链上的slice方法,把伪数组转换成真数组
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("div")); 

apply的应用场景:

  • Math.max,获取数组中最大、最小的一项
let max = Math.max.apply(null, array);

let min = Math.min.apply(null, array);
  • 实现两个数组合并

let arr1 = [1, 2, 3];

let arr2 = [4, 5, 6];

Array.prototype.push.apply(arr1, arr2);

console.log(arr1); // [1, 2, 3, 4, 5, 6]

bind的应用场景:

在vue或者react框架中,使用bind将定义的方法中的this指向当前类

四、实现一个 new 操作符

首先先来了解一下new 一个对象的过程

function Person(name) {
    this.name  = name
}
let son = new Person('小明');

1.创建一个对象

2.使空对象__proto__指向构造函数的原型(prototype)

3.新对象和函数调用的this会绑定起来,也就是把this绑定到空对象;

4.执行构造函数中的代码,为空对象添加属性

5.判断函数的返回值是否为对象,如果是对象,就使用构造函数的返回值,否则返回创建的对象

了解这个流程以后如何手写一个new呢?

function newOperator(ctor, ...args) {
    if (typeof ctor !== 'functoin') {
        throw new TypeError('Type ERROR')
    }
    let obj = {}; // 1
    ctor.__proto__ = obj.prototype; // 2
    let res = ctor.apply(obj, args); // 3、4

    let isObject  = res instanceof Object && res !== null;
    let isFunction = res instanceof Function;
    return isObject || isFunction ? res : obj
}

五、instanceof

实现原理:验证当前类的原型prototype是否会出现在实例的原型链__proto__上,只要在它的原型链上,则结果都为true。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,找到返回true,未找到返回false。

const myInstanceof = (left, right) => {
  // 由于instanceof的检测方法是判断检测的类型是否在当前实例的原型链上,不太适用于检测基本数据类型
  if(left !== 'object' || left === null) return false;
  let leftValue = left.__proto__
  let rightValue = right.prototype
  while(true) {
    if(leftValue === null) return false
    if(leftValue === rightValue) return true
    leftValue = leftValue.__proto__
  }
}
    leftValue = leftValue.__proto__

扩展: JavaScript判断变量的类型的几种方法

一共有4种方法判断变量的类型,分别是typeof、instanceof、Object.prototype.toString.call()(对象原型链判断方法)、 constructor (用于引用数据类型)

typeof:常用于判断基本数据类型,对于引用数据类型除了function返回’function‘,其余全部返回’object'。 instanceof:主要用于区分引用数据类型,检测方法是检测的类型在当前实例的原型链上,用其检测出来的结果都是true,不太适合用于简单数据类型的检测,检测过程繁琐且对于简单数据类型中的undefined, null, symbol检测不出来。

constructor:用于检测引用数据类型,检测方法是获取实例的构造函数判断和某个类是否相同,如果相同就说明该数据是符合那个数据类型的,这种方法不会把原型链上的其他类也加入进来,避免了原型链的干扰。 Object.prototype.toString.call():适用于所有类型的判断检测,检测方法是Object.prototype.toString.call(数据) 返回的是该数据类型的字符串。 Object.prototype.toString.call()原理:Object.prototype.toString 表示一个返回对象类型的字符串,call()方法可以改变this的指向,那么把Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果

这四种判断数据类型的方法中,各种数据类型都能检测且检测精准的就是Object.prototype.toString.call()这种方法。

六、 深拷贝

  • 判断类型是否为原始类型,如果是,无需拷贝,直接返回
  • 为避免出现循环引用,拷贝对象时先判断存储空间中是否存在当前对象,如果有就直接返回
  • 开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
  • 对引用类型递归拷贝直到属性为原始类型
const deepClone = (target, cache = new weakMap()) => {
    if(target === null || typeof target !== object) {
        return target;
    }
  
    if (cache.get(target)) {
        return target;
    }
  
  const copy = Array.isArray(target) ? [] : {};
  cache.set(target, copy);
  Object.keys(target).forEach(key => copy[key] = deepClone(target[key],cache))
  return copy
}

扩展: WeakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。WeakMap 的 key 只能是 Object 类型。 原始数据类型是不能作为 key 的(比如 Symbol)。

WeakMap具有的方法:

  • WeakMap.prototype.delete(key):移除 key 的关联对象。执行后 WeakMap.prototype.has(key) 返回false。
  • WeakMap.prototype.get(key):返回 key 关联对象,或者 undefined(没有 key 关联对象时)。
  • WeakMap.prototype.has(key):根据是否有 key 关联对象返回一个布尔值。
  • WeakMap.prototype.set(key, value):在 WeakMap 中设置一组 key 关联对象,返回这个 WeakMap 对象。
const wm1 = new WeakMap(),
      wm2 = new WeakMap(),
      wm3 = new WeakMap();
const o1 = {},
      o2 = function(){},
      o3 = window;
      
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value可以是任意值,包括一个对象或一个函数
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2中没有o2这个键
wm2.get(o3); // undefined,值就是undefined
wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是undefined)
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1);   // true
wm1.delete(o1);
wm1.has(o1);   // false

扩展: 常用深拷贝方法

  • JSON.parse(JSON.stringify())```

七、数组扁平化

数组扁平化是指将一个多维数组变为一个一维数组

方法一 使用flat()

const res1 = arr.flat(Infinity);

方法二: 利用正则, 但数据类型都会变为字符串

const res2 = JSON.stringify(arr).replace(/[|]/g, '').split(',');

方法三: 正则改良版本

const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/[|]/g, '') + ']');

方法四: 使用reduce

const flatten = arr => {

  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}

const res4 = flatten(arr);

方法五:函数递归

const res5 = [];
const fn = arr => {
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      fn(arr[i]);
    } else {
      res5.push(arr[i]);
    }
  }
}
fn(arr);

方法六:技巧变换

const str = [0, 1, [2, [3, 4]]].toString()

// '0, 1, 2, 3, 4'
const arr = str.split(',')
// ['0','1','2', '3', '4']
const newArr = arr.map(item => +item)
// [0, 1, 2, 3, 4]
const flatten = (arr) => arr.toString().split(',').map(item => +item)

八、数组去重

方法一: 利用Set

****const res1 = Array.from(new Set(arr));

方法二: 两层for循环+splice

const unique1 = arr => {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1);
        // 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能
        len--;
        j--;
      }
    }
  }
  return arr;
}


方法三: 利用indexOf

const unique2 = arr => {

  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
  }
  return res;
}

方法四: 利用include

const unique3 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!res.includes(arr[i])) res.push(arr[i]);
  }
  return res;
}

方法五: 利用filter

const unique4 = arr => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}


方法六: 利用Map

const unique5 = arr => {
  const map = new Map();
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true)
      res.push(arr[i]);
    }
  }
  return res;
}

九、继承

寄生组合继承

  function Super(foo) {
    this.foo = foo
  }
  Super.prototype.printFoo = function() {
    console.log(this.foo)
  }
  function Sub(bar) {
    this.bar = bar
    Super.call(this)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

ES6版继承

  class Super {
    constructor(foo) {
      this.foo = foo
    }
    printFoo() {
      console.log(this.foo)
    }
  }
  class Sub extends Super {
    constructor(foo, bar) {
      super(foo)
      this.bar = bar
    }
  }

十、类数组转换为数组

类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。

方法一:Array.from

Array.from(document.querySelectorAll('div'))复制代码

方法二:Array.prototype.slice.call()

Array.prototype.slice.call(document.querySelectorAll('div'))复制代码

方法三:扩展运算符

[...document.querySelectorAll('div')]复制代码

方法四:利用concat

Array.prototype.concat.apply([], document.querySelectorAll('div'));

十一、AJAX

const getJSON = function(url) {
  return new Promise((resolve, reject) => {
    const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
    xhr.open('GET', url, false);
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.onreadystatechange = function() {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    }
    xhr.send();
  })
}
    xhr.setRequestHeader('Accept', 'application/json');

十二、函数柯里化

指的是将一个接受多个参数的函数 变为 接受一个参数返回一个函数的固定形式,这样便于再次调用。

// 原理是利用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数
const currying = fn =>
    _curry = (...args) => 
        args.length >= fn.length
        ? fn(...args)
        : (...newArgs) => _curry(...args, ...newArgs)

参考文章: 作者:洛霞同学 链接:juejin.cn/post/687515…