call、apply的模拟实现——手写题

128 阅读7分钟

你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹

call、apply的模拟实现

call与apply的异同

ECAMScript 3给Function的原型定义了两个方法,它们是Function.prototype.callFunction.prototype.apply它们的作用一模一样,区别仅在于传入参数形式的不同。

apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数

call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数:

MDN对apply参数的介绍

function.apply(thisArg)
function.apply(thisArg, argsArray)

thisArg

func 函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

argsArray 可选

一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 nullundefined,则表示不需要传入任何参数。从 ECMAScript 5 开始可以使用类数组对象。

返回值

调用有指定 this 值和参数的函数的结果。

MDN对call参数的介绍

function.call(thisArg, arg1, arg2, ...)

thisArg

可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。 arg1, arg2, ...

指定的参数列表。

模拟实现得先知道实现的规范,那先得看看ES5规范。ES5规范 英文版ES5规范 中文版

当以 thisArg 和 argArray 为参数在一个 func 对象上调用 apply 方法,采用如下步骤:

  1. 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常 .
  2. 如果 argArray 是 null 或 undefined, 则
    1. 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。
  3. 如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 .
  4. 令 len 为以 "length" 作为参数调用 argArray 的 [[Get]] 内部方法的结果。
  5. 令 n 为 ToUint32(len).
  6. 令 argList 为一个空列表 .
  7. 令 index 为 0.
  8. 只要 index < n 就重复
    1. 令 indexName 为 ToString(index).
    2. 令 nextArg 为以 indexName 作为参数调用 argArray 的 [[Get]] 内部方法的结果。
    3. 将 nextArg 作为最后一个元素插入到 argList 里。
    4. 设定 index 为 index + 1.
  9. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。

apply 方法的 length 属性是 2。

在外面传入的 thisArg 值会修改并成为 this 值。thisArg 是 undefined 或 null 时它会被替换成全局对象,所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。

首先看这段代码,实现apply重要的一点是实现this指向的改变

function fun() {
  console.log(this.name);
}

const obj = {
  name: 'abc'
}

fun.apply(obj) // abc

obj.fn = fun
obj.fn()    //abc

要实现函数内部this指向的改变,除了用apply改变this指向,将函数当做对象的方法,再用对象调用同样也可以实现,基于这个可以模拟实现一个简易apply

function getGlobalThis() {
  return this
}

Function.prototype.myApply = function (thisArg, argArray) {
  //1.如果调用apply不是函数,抛出TypeError
  if (typeof this !== 'function') {
    return new TypeError(this + ' is not a function')
  }
  // 2.如果 argArray 是 null 或 undefined, 则返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。
  if (typeof argArray === 'undefined' || argArray === null) {
    argArray = [];
  }

  //3.如果参数argArray不是Object抛出TypeError
  if (!(argArray instanceof Object)) {
    return new TypeError('CreateListFromArrayLike called on non-object')
  }
  //在外面传入的 thisArg 值会修改并成为 this 值。
  //ES3: thisArg 是 undefined 或 null 时它会被替换成全局对
  if (typeof thisArg === 'undefined' || thisArg === null) {
    thisArg = getGlobalThis()
  }
  //ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值,就是将原始值变成对象
  thisArg = new Object(thisArg)
  //9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。
  thisArg['__fn'] = this;
  var result = thisArg['__fn'](...argArray)
  delete thisArg['__fn']
  return result;
}

function fun() {
  console.log(this.name);
}

const obj = {
  name: 'abc'
}

fun.myApply(obj) // abc

提出问题

  • 1、但是这样会有一个问题,同名属性覆盖,假如对象内有_fn属性,就会被覆盖并删除

解决办法:

  1. 用Symbol类型做方法名,但是Symnol是ES6才有的,这里是模拟ES3apply方法
Symbol('__fn')
  1. Math.random() 随机生成一个数字当方法名

    '__' + Math.random()
    
  2. 用时间戳当方法名

'__' + new Date().getTime()

万一key还是重复了,保险起见可以做一步缓存

  //做缓存,防止覆盖key
  let originalVal = thisArg['__fn']
  //是否含有该属性
  let hasOriginalVal = thisArg.hasOwnProperty('__fn')
  thisArg['__fn'] = this;
  let result = thisArg['__fn'](...argArray)
  delete thisArg['__fn']
  //如果有,重新赋值回去
  if (hasOriginalVal) {
    thisArg['__fn'] = originalVal;
  }
  return result;
  • 2、另一个问题,使用了ES6扩展符...

解决方法:

只能采用new Function() 创建函数的方法将参数数组展开

//获得函数代码
function generateFunctionCode(argsArrayLength) {
  var code = 'return arguments[0][arguments[1]]('
  for (var i = 0; i < argsArrayLength; i++) {
    if (i > 0) {
      code += ',';
    }
    code += 'arguments[2][' + i + ']'
  }
  code += ')';
  // return arguments[0][arguments[1]](arg1, arg2, arg3...)
  return code
}

var code = generateFunctionCode(argArray.length)
var result = (new Function(code))(thisArg, '__fn', argArray)

代码解释:这里用自执行函数将参数thisArg, '__fn', argArray传入调用new Function返回的函数,这里arguments[0]就等于thisArg,前面我们给thisArg加入一个__fn的属性,arguments[1]就是字符串"__fn"arguments[0][arguments[1]]就是取出thisArg里面的__fn函数__fnarguments[2]就是传入apply函数的参数数组argArrayarguments[2][0]就是数组第一个元素,以此类推

//获取全局对象
function getGlobalThis() {
  return this
}

//获得函数代码
function generateFunctionCode(argsArrayLength) {
  var code = 'return arguments[0][arguments[1]]('
  for (var i = 0; i < argsArrayLength; i++) {
    if (i > 0) {
      code += ',';
    }
    code += 'arguments[2][' + i + ']'
  }
  code += ')';
  // return arguments[0][arguments[1]](arg1, arg2, arg3...)
  return code
}

Function.prototype.myApply = function (thisArg, argArray) {
  //1.如果调用apply不是函数,抛出TypeError
  if (typeof this !== 'function') {
    return new TypeError(this + ' is not a function')
  }
  // 2.如果 argArray 是 null 或 undefined, 则返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。
  if (typeof argArray === 'undefined' || argArray === null) {
    argArray = [];
  }

  //3.如果参数argArray不是Object抛出TypeError
  if (!(argArray instanceof Object)) {
    return new TypeError('CreateListFromArrayLike called on non-object')
  }
  //在外面传入的 thisArg 值会修改并成为 this 值。
  //ES3: thisArg 是 undefined 或 null 时它会被替换成全局对像
  if (typeof thisArg === 'undefined' || thisArg === null) {
    thisArg = getGlobalThis()
  }
  //ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值,就是将原始值变成对象
  thisArg = new Object(thisArg)
  //9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。

  //做缓存,防止覆盖key
  var originalVal = thisArg['__fn']
  //是否含有该属性
  var hasOriginalVal = thisArg.hasOwnProperty('__fn')
  thisArg['__fn'] = this;
  //ES6实现法: var result = thisArg['__fn'](...argArray)
  var code = generateFunctionCode(argArray.length)
  var result = (new Function(code))(thisArg, '__fn', argArray)
  delete thisArg['__fn']
  //如果有,重新赋值回去
  if (hasOriginalVal) {
    thisArg['__fn'] = originalVal;
  }
  return result;
}

在ES3,ES5中,undefined是可以被修改的

可能大部分人不知道。ES5中虽然在全局作用域下不能修改,但在局部作用域中也是能修改的,以下代码可以证实这点

function test() {
  var undefined = 3;
  console.log(undefined); // chrome下也是 3
  console.log(typeof undefined); //number
}
test();

因此判断一个值是否为undefined,更严谨的方案是typeof a === 'undefined'或者a === void 0; 这里面用的是voidvoid的作用是计算表达式,始终返回undefined,也可以这样写void(0)。 解决了这几个问题,比较容易实现如下代码。

利用模拟实现的apply,模拟实现call

Function.prototype.myCall = function (thisArg) {
  //把后面的参数,都放进数组里面就可以调用apply方法了
  var argsArray = [];
  var argumentsLength = arguments.length;
  for (var i = 0; i < argumentsLength - 1; i++) {
    argsArray[i] = arguments[i + 1]
  }
  console.log('argsArray:', argsArray);
  return this.myApply(thisArg, argsArray);
}

function fun(a, b) {
  console.log(this.name, a, b);
}

const obj = {
  name: 'abc'
}

fun.myApply(obj, [1, 2]) // abc 1 2
fun.myCall(obj, 1, 2)    // abc 1 2

参考文章

面试官问:能否模拟实现JS的call和apply方法

MDN-apply

MDN-call