还是老规矩,动手写代码前先复习一下:
方法 | 参数 | 返回值 | 作用 |
---|---|---|---|
call |
thisArg, arg1, arg2, ... |
函数的返回值 | 调用一个函数,将其 this 值设置为提供的值,并为其提供指定的参数。 |
apply |
thisArg, [arg1, arg2, ...] |
函数的返回值 | 调用一个函数,将其 this 值设置为提供的值,并以数组形式为其提供参数。 |
bind |
thisArg, arg1, arg2, ... |
新函数 | 创建一个新函数,当调用这个新函数时,它的 this 值被绑定到提供的值,并且它的参数序列前置指定的参数。 |
call
在手写一个原生的 API 之前,我们要先知道它在使用时的各种表现,才能更贴近地实现其功能。比如在上一期每天搞透一道JS手写题💪「Day3手写数组的六种原型方法中笔者提到的要用 Object.prototype.hasOwnProperty.call(this, i)
在遍历中判断当前数组的当前位置是空还是值为 undefined
,就是因为数组原型方法中对这两者的处理是不一样的。
那么以 call
方法为例,它有哪些特点呢:
- 调用对象应该是一个函数,否则会抛出类型错误。
- 在非严格模式下,如果绑定的
thisArg
为null
或undefined
,thisArg
会被指向 globalThis。 - 在严格模式下,如果绑定的
thisArg
为null
或undefined
,thisArg
会保持原来的值。 - 如果绑定的
thisArg
不是引用数据类型,会被转换成对应的引用数据类型,如 1 变成 Number(1)。
核心的实现思路是,在需要绑定的 thisArg
指向的对象上创建一个调用 call
方法的函数的副本,在函数调用完毕后删除这个副本属性。接下来我们按照这个核心思路,同时注意原生 call
的特点,来实现我们的 myCall
:
js
/**
* 实现函数的call方法
* @param {Object} thisArg - 函数运行时使用的this值
* @param {...*} args - 传递给函数的参数列表
* @returns {*} 函数执行的返回值
*/
Function.prototype.myCall = function(thisArg, ...args) {
// 判断是否在严格模式下运行
const isStrict = (function() { return !this; })();
// 处理thisArg的值
if (thisArg === null || thisArg === undefined) {
thisArg = isStrict ? undefined : globalThis;
} else {
thisArg = Object(thisArg);
}
// 将函数作为thisArg的一个属性
const fn = Symbol('fn');
thisArg[fn] = this;
// 调用函数并获取返回值
const result = thisArg[fn](...args);
// 删除添加的属性
delete thisArg[fn];
// 返回结果
return result;
}
- 在非严格模式下,函数独立调用
this
会指向window
;而在严格模式下,this
会是undefined
。 - 使用
Symbol
来创建一个唯一的属性名,这一步的目的是防止thisArg
对象有同名的属性导致其被临时的副本属性覆盖。
apply
apply
和 call
的功能基本一致,所以实现起来的核心思路也是一样的。它们的主要区别在于如何传递参数:
call
直接传递一个参数列表,而apply
接受一个参数数组。这也为它带来了一个区别于call
的特点:如果传给apply
的第二个参数不是一个数组,会抛出一个错误'CreateListFromArrayLike called on non-object'
。- 第二个参数如果是
null
或undefined
,在call
中会被传递给调用的函数,但在apply
中会被视为空数组,调用apply
的函数不会有任何参数。
js
/**
* 实现函数的apply方法
* @param {Object} thisArg - 函数运行时使用的this值
* @param {Array} argsArray - 传递给函数的参数数组
* @returns {*} 函数执行的返回值
*/
Function.prototype.myApply = function(thisArg, argsArray) {
// 判断是否在严格模式下运行
const isStrict = (function() { return !this; })();
// 处理thisArg的值
if (thisArg === null || thisArg === undefined) {
thisArg = isStrict ? undefined : globalThis;
} else {
thisArg = Object(thisArg);
}
// 检查argsArray是否为数组或类数组对象
if (!Array.isArray(argsArray) && !(argsArray instanceof Object && 'length' in argsArray) && (argsArray !== null && argsArray !== undefined)) {
throw new TypeError('CreateListFromArrayLike called on non-object');
}
// 将函数作为thisArg的一个属性
const fn = Symbol('fn');
thisArg[fn] = this;
// 调用函数并获取返回值, 展开运算符对 null 或 undefined 使用会报错,故需判断
const result = argsArray ? thisArg[fn](...argsArray) : thisArg[fn]();
// 删除添加的属性
delete thisArg[fn];
// 返回结果
return result;
}
bind
bind
方法和前面两种方法不同,它是创建了一个新的函数,这个新的函数的 this
值永远指向绑定的 thisArg
对象。它也具备 call
方法的那四个特点,额外还有一个需要注意的特点:
- 如果绑定后的函数被用作构造函数,那么新创建的对象会作为
thisArg
值传递给原函数。
js
/**
* 实现函数的bind方法
* @param {Object} thisArg - 函数运行时使用的this值
* @param {...*} args - 预设的参数列表
* @returns {Function} 绑定后的新函数
*/
Function.prototype.myBind = function(thisArg, ...args) {
// 检查调用对象是否为函数
if (typeof this !== 'function') {
throw new TypeError('Bind must be called on a function');
}
// 判断是否在严格模式下运行
const isStrict = (function() { return !this; })();
// 处理thisArg的值
if (thisArg === null || thisArg === undefined) {
thisArg = isStrict ? undefined : globalThis;
} else {
thisArg = Object(thisArg);
}
// 获取原函数引用
const self = this;
// 返回一个新函数,该新函数绑定了this值和预设参数列表
return function(...newArgs) {
// 如果作为构造函数调用,则忽略传入的thisArg,使用新创建的对象作为this值
if (new.target) {
thisArg = this;
}
// 调用原函数并返回结果,将预设参数和新传入参数拼接起来传递给原函数
return self.apply(thisArg, [...args, ...newArgs]);
}
}
作为构造函数调用的情况可能有一些不好理解,这里举个例子:
js
// 定义一个测试函数
function TestFunc(name) {
this.name = name;
}
// 使用bind方法绑定this值和参数
const BoundFunc = TestFunc.bind(null, 'Alice');
// 使用new运算符调用绑定后的函数
const obj = new BoundFunc();
// 输出: Alice
console.log(obj.name);
可以看到,即使绑定的 thisArg
是 null
,最后的 this
仍然指向了新建的 obj
。