面试官:这些 JavaScript API 怎么实现

77 阅读5分钟

前言

在面试时,要求模拟applycallnewObject.createinstanceofJavaScript 基础API 实现方式便是家常便饭的事,笔者前两天就遇到这种好事,在此总结了它们的实现方式

call 实现

先看看在 JavaScript 中调用方式吧:

const foo = {};
function bar(){
  console.log(this); // foo {}
}
bar.call(foo);

分析一下它的作用:

  • 改变函数的 this 指向
  • 调用函数,并传递参数

经过分析,我们知道了 call 的作用,那么如何实现?

this 指向改变

通俗理解 this 的话,一般可以理解为:谁调用,this 指向谁。根据上面特性,我们可以简单写出以下代码:

Function.prototype.call = function(context){
  context.fn = this;
  context.fn();
  delete context.fn;
}

当我们传入 context 时,我们往其上面添加一个属性,值为函数本身,在调用它,这样函数的 this 指向就是传入的 context 了,最后删除添加属性。

当前我们需要考虑一些边界场景

  • 传入 context 不是一个对象
  • 添加属性名考量,防止覆盖对象同名属性

于是,我们继续优化代码

Function.prototype.call = function(context) {
  context = toObject(context);
  const symbol = Symbol();
  context[symbol] = this;
  const val = context[symbol]();
  delete context[symbol];
  return val;
}
  
// 处理null值和undefined值,以及基本类型
function toObject(val){
  const type = typeof val;
  let result = val;
  switch(type){
    case "number":
    case "boolean":
    case "string":
      result = Object(val);
      break;
    default:
      result  = result || window;
  }
  return result;
}

在一切开始之前,我们需要将 context 转换为对象,防止阻断后续流程。在添加属性时,我们以 symbol 作为属性名,添加到 context 上,设置一个唯一属性,这样就不会覆盖同名属性了

参数传递

接下来我们讨论第二点:函数执行和参数传递。 call 在调用时,它会把原先函数的参数一个个传递过来。

const context = {}
function add(a, b, c){
  console.log(a, b, c)
}
// 输出 1, 2, 3
add.call(context, 1, 2, 3)

于是,我们很容易想到可以使用 剩余参数 方式实现它

Function.prototype.call = function(context, ...rest) {
  context = toObject(context);
  const symbol = Symbol();
  context[symbol] = this;
  const val = context[symbol](...rest);
  delete context[symbol];
  return val;
}

apply 实现

applycall 的最大区别在于:apply 传递参数是一个数组

const context = {}
function add (a, b, c){
  console.log(a, b, c)
}

add.apply(context, [1, 2, 3])

我们可以完全扩展引用 call 的实现,并额外处理参数

Function.prototype.call = function(context, args = []) {
  args = Array.from(args)
  context = toObject(context);
  const symbol = Symbol();
  context[symbol] = this;
  const val = context[symbol](...args);
  delete context[symbol];
  return val;
}

bind实现

bindcall 也有类似之处,bind 只改变函数 this 指向,但不会调用函数,并且会返回一个新函数,此外它还有点 柯里化 的特征。比如以下示例

const context = {}
function add (a, b, c){
  console.log(a, b, c)
}

const newAdd = add.bind(context, 1)
newAdd(2, 3)

我们在实现它时,需要考虑以下几点:

  • 改变函数 this 指向
  • 返回新函数,新函数和原函数基本相同
  • 参数合并

改变函数 this 指向

我们完全可以使用上面成果来实现

Function.prototype.bind = function(context, ...args){
  this.apply(context, args)
}

返回新函数,新函数和原函数相同

要使新函数和原函数一致,那么我们可以使用继承方式

Function.prototype.bind = function(context, ...args){
  const self = this
  function f(){
    return self.apply(context, args);
  }
  f.prototype = Object.create(self.prototype);
  return f;
}

但是需要注意一个特殊现象,再此之前,大家可以先猜猜这段代码执行输出什么?

const context = {
  name: 'cola'
}

function Person () {
  console.log(this.name)
}
Person.prototype.name = 'xiaoming'
const bindPerson = Person.bind(context)
new bindPerson() // xiaoming
bindPerson() // cola

如果按照我们上面实现,应该会打印两次 cola,但是只打印了一次,所以我们需要额外处理实例化场景,那么对于 this 指向需要稍微调整。

function f(){
  return self.apply(this instanceof f ? this : context, args);
}

参数合并

bind 函数参数可以传递到最终函数中,那么我们需要进行参数合并

Function.prototype.bind = function(context, ...args){
  const self = this;
  function f(){
    const _args = [...args, ...arguments];
    return self.apply(this instanceof f ? this : context, _args);
  }
  f.prototype = Object.create(self.prototype);
  return f;
}

new实现

在前面的 bind 实现中,我们稍微提到了实例化场景,那么这节我们了解下 new 到底干了什么?先举个例子

function Person(name, age){
  this.name = name
  this.age = age
}
Person.prototype.getName = function() {
    return this.name
}

const p = new Person('xiaoming', 18)
console.log(p.name) // xiaoming
console.log(p.getName()) // xiaoming

通过上面例子,我们大概明白了 new 的功能

  • 执行函数,返回一个新对象
  • 改变 this 指向,this 指向新对象
  • 传递参数
  • 实现继承

执行函数

执行一个函数是非常简单的,但需要配合改变 this 指向,我们可以使用上文的 apply 方式

function _new (constructor, ...args) {
  constructor.apply(null, args);
}

实现继承

通过上面例子,我们知道 new 会返回一个新对象,新对象会继承传入构造函数的原型,并且新对象上面挂载了构造函数的属性,那么我们可以使用 Object.create 创建一个新对象,并把新对象作为构造函数的 this 指向

function _new(constructor, ...args) {
  const obj = Object.create(constructor.prototype);
  constructor.apply(obj, args);
  return obj
}

但是需要注意一点,当 构造函数有返回值的时候,那么 new 返回的是构造函数执行结果

function Person(){
  this.name = name
  return {
    name: 'xiaohong'
  }
}

const p = new Person('xiaoming')
console.log(p.name)

所以我们需要修正一下:

function _new(constructor,...args){
  const obj = Object.create(constructor.prototype);
  const res = constructor.apply(obj, args);
  return (typeof res === 'object' && res !== null) ? res : obj;
}

Object.create实现

Object.create 是用于创建一个新对象,并且存在继承关系,举个例子

function Parent(){}
Parent.prototype.hello = function (){
   console.log('hello world')
}
function Child(){}

Child.prototype = Object.create(Parent.prototype)
const child = new Child()
console.log(child.hello())

通过示例,我们大概明白 Object.create 的功能:

  • 创建新的对象
  • 实现继承关系

创建新对象

创建对象我们可以使用函数式或者声明式

const obj_1 = Object()
const obj_2 = {}

实现继承关系

实现继承,我们只需要改变原型链即可

Object.create = function(prototype){
  const obj = {}
  Object.setPrototypeOf(obj, prototype)
  return obj
}

当然,我们可以使用上文的 new 来实现

Object.create = function(prototype){
  function f(){};
  f.prototype = prototype;
  return new f();
}

instanceof 实现

检查构造函数的 prototype 属性是否出现在 实例对象 的原型链上,那么我们只需要不断向上查询实例对象的原型链,同时和当前构造函数的 prototype 对比即可,相同则返回 true

function instanceOf(instance, constructor) {
  let proto = Object.getPrototypeOf(instance);
  const prototype = constructor.prototype;
  while(proto !== null){
    if(proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false
}

那么我们需不需要考虑循环引用的事情呢?其实是不需要的

function instanceOf(instance, constructor) {
  let proto = Object.getPrototypeOf(instance);
  const prototype = constructor.prototype;
  while(proto !== null){
    if(proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false
}

function Parent(){
    this.name = 'parent'
}

function Child(){
    this.name = 'child'
}

function GrandChild(){
    this.name = 'grandchild'
}


Child.prototype.__proto__ = Parent.prototype
Child.prototype.constructor = Child

GrandChild.prototype.__proto__ = Child.prototype
GrandChild.prototype.constructor = GrandChild

Parent.prototype.__proto__ = GrandChild.prototype
Parent.prototype.constructor = Parent

const child = new Child()
console.log(child)
console.log(child instanceof Parent)
console.log(instanceOf(child, Parent))

当我们将 Parent.prototype 指向 GrandChild.prototype时,其实构成了循环引用,如果存在循环引用,JavaScript 会抛出一个错误,所以无需担心这个问题

image.png