你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹
call、apply的模拟实现
call与apply的异同
ECAMScript 3给Function的原型定义了两个方法,它们是Function.prototype.call和Function.prototype.apply它们的作用一模一样,区别仅在于传入参数形式的不同。
apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数
call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数:
MDN对apply参数的介绍
function.apply(thisArg)
function.apply(thisArg, argsArray)
thisArg
在 func 函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
argsArray 可选
一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从 ECMAScript 5 开始可以使用类数组对象。
返回值
调用有指定 this 值和参数的函数的结果。
MDN对call参数的介绍
function.call(thisArg, arg1, arg2, ...)
thisArg
可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
arg1, arg2, ...
指定的参数列表。
模拟实现得先知道实现的规范,那先得看看ES5规范。ES5规范 英文版,ES5规范 中文版。
当以 thisArg 和 argArray 为参数在一个 func 对象上调用 apply 方法,采用如下步骤:
- 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常 .
- 如果 argArray 是 null 或 undefined, 则
- 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。
- 如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 .
- 令 len 为以 "length" 作为参数调用 argArray 的 [[Get]] 内部方法的结果。
- 令 n 为 ToUint32(len).
- 令 argList 为一个空列表 .
- 令 index 为 0.
- 只要 index < n 就重复
- 令 indexName 为 ToString(index).
- 令 nextArg 为以 indexName 作为参数调用 argArray 的 [[Get]] 内部方法的结果。
- 将 nextArg 作为最后一个元素插入到 argList 里。
- 设定 index 为 index + 1.
- 提供 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属性,就会被覆盖并删除
解决办法:
- 用Symbol类型做方法名,但是
Symnol是ES6才有的,这里是模拟ES3apply方法Symbol('__fn')
Math.random() 随机生成一个数字当方法名
'__' + Math.random()用时间戳当方法名
'__' + 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函数__fn,arguments[2]就是传入apply函数的参数数组argArray,arguments[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; 这里面用的是void,void的作用是计算表达式,始终返回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