面试考过的JSON.stringify、new、instanceof、apply等自定义实现

80 阅读3分钟

现在看来学习自定义某个内部Api不是重点,重点是掌握Api的原理。

曾经的面试被要求实现下面几个内部功能函数。

自定义JSON.stringify

    function getType (vari) {
      return Object.prototype.toString.call(vari).slice(8, -1)
    }
    function toStringify (obj) {
      let str = ''
      switch (getType(obj)) {
        case 'Object': // 对象类型需要遍历递归
          str += '{'
          Object.keys(obj).forEach((k) => {
            const res = toStringify(obj[k])
            if (res) {
              str += `"${k}":${res},`
            }
          })
          return str.slice(0, -1) + '}'
        case 'Array': // 数组类型需要遍历递归
          str = '['
          obj.forEach((k) => {
            const res = toStringify(k)
            if (res) {
              str += res + ','
            }
          })
          return str.slice(0, -1) + ']'
        case 'Date': // 日期类型需要toJSON转日期字符串
          return `"${obj.toJSON()}"`
        case 'String': // 字符串需要加引号
          return `"${obj}"`
        case 'Number': // 数字类型直接返回
          return `${obj}`
        case 'Boolean': // 布尔类型true和false转字符串
          if (obj === true) {
            return 'true'
          }
          return 'false'
        case 'RegExp': // 以下类型直接转空对象字符串
        case 'Set':
        case 'Map':
        case 'WeakMap':
        case 'WeakSet':
        case 'ArrayBuffer':
        case 'Blob':
        case 'Int32Array':
        case 'Int8Array':
        case 'Int16Array':
          return '{}'
        case 'Null': // null转字符串
          return 'null'
        case 'BigInt': // bigint类型报错
          throw Error('Do not know how to serialize a BigInt')
        case 'Function': // 以下三个及其它类型返回空忽略
        case 'Undefined':
        case 'Symbol':
        default:
      }
    }

当初被考到自定义实现JSON.stringify时候,一脸懵逼。面试肯定不用想没过就是了。这个东西就是将对象数据一下字符串化了。自己来实现的话就是根据输入的对象类型做针对性的处理。需要着重处理ObjectArrayStringDate等类型。

当初对数据的理解还是不到位。数据本来就是可以相互转化的,也包括这种形式的转化。

自定义new操作符

// 自定义new操作符

function mynew(...args){
    const constrctor = args.shift()
   // 创建一个空对象obj,使这个空对象继承构造函数的prototype属性
    const obj = Object.create(constrctor.prototype)
    const result = constrctor.apply(obj, args)

    return (typeof result === 'object' && result != null) ? result : obj
}

const test = function (name){
    this.name = name
}
console.log(mynew(test, 'test'))

new操作符做了4件事:

  1. 创建一个空对象
  2. 继承原型属性
  3. 改变对象this指向,执行构造函数
  4. 返回对象

如果构造函数有返回值,且返回的是对象且不为null则替换new操作符结果,否则就返回对象实例。

自定义数组map函数

Array.prototype.newMap = function(fn) {
   var newArr = [];
   for(var i = 0; i<this.length; i++){
      newArr.push(fn(this[i],i,this))
   }
   return newArr;
}

自定义instanceof

// 自定义个instanceof 函数 困难
const myInstanceof = (Left, Right)=>{
    if(typeof Left!=='object') {
        return false
    }
  
    while(true) {
        if(Left === null) {
            return false
        }
        // 关键点1
        if(Right.prototype === Left.__proto__) {
            return true
        }
        // 关键点2
        Left = Left.__proto__
    }
  }
  
  console.log(myInstanceof({}, Object))

理解上面自定义instanceof就能理解原型链。关键点是__proto__可以一直回溯,一直到null为止。若__proto__上面有节点等于Right.prototype则Left是Right的实例。所以Right.prototype是__proto__上的一个节点。

一般情况下声明的对象字面量自带原型属性,如果想去掉原型属性可以用Object.create(null)。有段时间老是被问原型链的问题。

image.png

自定义bind函数

 // 自定义bind
 Function.prototype.mybind = Function.prototype.bind || function(context) {
    var me = this
    // 去除第一个元素
    var args = Array.prototype.slice.call(arguments, 1)
    var F= function(){}
    F.prototype = this.prototype
    var bound =  function(){
        // 类数组转为数组
        var innerArgs = Array.prototype.slice.call(arguments)
        var finalArgs = args.contact(innerArgs)
        return me.apply(this instanceof F ? this : context || this, finalArgs)
    }
    bound.prototype = new F()
    return bound;
  
  }
  const foo = {
    value: 1
  };
  const test = function(){
    console.log(this.value);
  }
  console.log(test.bind(foo)())

改变this指向的方法有applycall,两者只是参数不同。apply第二个参数为数组,call为参数序列。还有bind。上面自定义考虑了如果bind返回的函数作为构造函数时的情况,这时new的优先级更高。

自定义apply函数

 Function.prototype.applyFn = function (targetObject, argsArray){
          if(typeof argsArray === 'undefined' || argsArray === null) {
            argsArray = []
          }
          if(typeof targetObject === 'undefined' || targetObject === null) {
            targetObject = window
          }
          targetObject = new Object(targetObject)
          const targetFnKey = 'targetFnKey' // 此时要求原有对象没有该属性否则会被覆盖
          targetObject[targetFnKey] = this // this指调用的函数

          const result = targetObject[targetFnKey](...argsArray) // 隐式绑定 this作为targetObject内部函数调用时 this指向targetObject
          delete targetObject[targetFnKey]
          return result
      }

上述自定义实现有个隐式绑定,也就是将函数作为对象的一个属性,之后调用函数时,其中的this自然指向对象。

学习自定义内部功能Api不光是为了更好地面试,也是为了加深对相应功能Api的理解。理解深入了自然能够运用的得心应手。本文只是抛砖引玉,其实还有别的面试可以考的自定义内部功能函数,比如数组的reduce函数,感兴趣的可以探索实现。