在聊我们的主题前我们先聊下函数中this的确定
1 this的确定
在上篇文章中说到this的确定是发生在执行上下文创建阶段,执行上下文是每次函数执行都会创建的并且是唯一的,那么也可以说this是动态确定的,而不是根据代码定义的位置确定的,如:
let obj = {
"name": "小王",
"getName": function () {
return this.name;
}
};
let obj2 = {
name: "我才是小王"
}
obj.getName(); // 小王 此时的this.name === obj.name 属于显示绑定
const getName = obj.getName;
getName(); // 此时this.name === window.name 属于隐式绑定
执行obj.getName函数输出小王,符合预期;然而,通过obj.getName获取了getName,直接执行getName函数,这时候结果是获取了window对象的name属性(window.name),也就是说this是window。
那我们有什么方式在函数运行的时候更改
this指向呢?
JavaScript中Function的原型为我们提供了call、apply和bind方法可以将 this 值绑定到调用中的特定对象。看下面
getName.call(obj2) // 我才是小王
getName.apply(obj2) // 我才是小王
getName.bind(obj2)() // 我才是小王
当函数作为构造函数(通过new创建)时,this的指向其实正在构造的新对象,具体原因可以参考这篇文章
this的确定有一下几种情况,优先级有大到小:
- 作为构造函数(通过
new创建),this指向正在构造的新对象;- 通过
call、apply和bind绑定指定对象,this指向绑定的对象;- 最为dom元素事件处理函数,
this指向触发事件的元素;作为dom内联事件处理函数,this指向dom元素;MDN- 显示绑定:如上文的
obj.getName()- 默认绑定:上面都不能确定的话,
浏览器环境为window,node环境为global,在严格模式下为undefined。
下面我们来聊一下本文的主题call、apply和bind
2 call语法
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
语法::func.call(thisArg, arg1, arg2, ...)
thisArg:在function函数运行时使用的this值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为null或undefined时会自动替换为指向全局对象,原始值会被包装。arg1, arg2, ...指定的参数列表。
3 call实现
- 第一个参数(
thisArg),如果传null或undefined默认为window - 接下来给
thisArg创建一个fn属性,并将值设置为需要调用的函数 - 调用函数并将对象上的函数删除
- 返回调用函数返回值
const isFunc = obj => typeof obj === "function"
Function.prototype.customCall = function (thisArg, ...args) {
if (isFunc(thisArg)) throw new TypeError(thisArg + ".customCall is not a function")
let context = thisArg == null ? window : thisArg;
if (!/^(object|function)$/i.test(typeof context)) {
context = Object(context)
}
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
}
4 apply语法
apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或类似数组对象)提供的参数。
语法:func.apply(thisArg, [argsArray])
thisArg:在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为null或undefined时会自动替换为指向全局对象,原始值会被包装。argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为null或undefined,则表示不需要传入任何参数。
但是 argsArray 参数是会有超出JavaScript引擎的参数长度限制的风险,当你对一个方法传入非常多的参数(比如一万个)时,就非常有可能会导致越界问题, 这个临界值是根据不同的 JavaScript 引擎而定的(JavaScript 核心中已经做了硬编码参数个数限制在65536),因为这个限制(实际上也是任何用到超大栈空间的行为的自然表现)是未指定的. 有些引擎会抛出异常。更糟糕的是其他引擎会直接限制传入到方法的参数个数,导致参数丢失。举个例子:如果某个引擎限制了方法参数最多为4个(实际真正的参数个数限制当然要高得多了, 这里只是打个比方), 上面的代码中, 真正通过 apply传到目标方法中的参数为 5, 6, 2, 3 而不是完整的数组。
如果你的参数数组可能非常大,那么可以使用将参数数组切块后循环传入目标方法的策略。如:
function minOfArray(arr) {
let min = Infinity;
const QUANTUM = 32768;
for (let i = 0, len = arr.length; i < len; i += QUANTUM) {
const submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
min = Math.min(submin, min);
}
return min;
}
var min = minOfArray([5, 6, 2, 3, 7]);
5 apply实现
apply的实现与call的实现类似,不同的是对于参数的处理
const isFunc = obj => typeof obj === "function"
Function.prototype.customApplay = function (thisArg, argsArray = []) {
if (isFunc(thisArg)) throw new TypeError(thisArg + ".customApplay is not a function")
let context = thisArg == null ? window : thisArg;
if (!/^(object|function)$/i.test(typeof context)) {
context = Object(context)
}
context.fn = this;
const result = context.fn(...argsArray);
delete context.fn;
return result;
}
6 bind语法
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法:func.bind(thisArg[, arg1[, arg2[, ...]]])
- 参数
-
thisArg调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
-
arg1, arg2, ...当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
- 返回值
-
- 返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
7 bind实现
bind 的实现对比其他两个函数略微地复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题。
const isFunc = obj => typeof obj === "function"
Function.prototype.customBind = function (thisArg, ...args) {
if (isFunc(thisArg)) throw new TypeError(thisArg + ".customBind is not a function")
let context = thisArg == null ? window : thisArg;
if (!/^(object|function)$/i.test(typeof context)) {
context = Object(context)
}
const _this = this;
return function B () {
if (this instanceof B) {
return new _this(...args, ...arguments);
}
return _this.call(context, ...args, ...arguments);
}
}
以下是对实现的分析:
- 前几步和之前的实现差不多,就不赘述了
- bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
- 对于直接调用来说,这里选择了
call的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现...args, ...arguments - 最后来说通过
new的方式,在上文中我们有提到对于new的情况来说,不会被任何方式改变this,所以对于这种情况我们需要忽略传入的this。
8 call、apply和bind的对比
- call和apply调用的时候会立即执行,不同的是call传入的是参数列表,apply传入的是参数数组或类数组对象;
- bind调用只是返回了一个新的函数,并且传入的第1个以后的参数会与后续调用返回的函数合并传入到目的函数;当通过 new 的方式调用返回的函数时,指定的
this会被忽略。 - apply传入的
argsArray参数是会有超出JavaScript引擎的参数长度限制的风险;call没有。 - call的性能会比apply的要高点。