this 的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把 this 固定下来,避免出现意想不到的情况。JavaScript 提供了 call、apply、bind 这三个方法,来切换/固定 this 的指向。
call
函数实例的 call 方法,可以指定函数内部 this 的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
上面代码中,全局环境运行函数 f 时,this 指向全局环境(浏览器为 window 对象);call 方法可以改变 this 的指向,指定 this 指向对象 obj,然后在对象 obj 的作用域中运行函数 f。
call 方法的参数,应该是一个对象。如果参数为空、null 和 undefined,则默认传入全局对象。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
上面代码中,a 函数中的 this 关键字,如果指向全局对象,返回结果为 123。如果使用 call 方法将 this 关键字指向 obj 对象,返回结果为 456。可以看到,如果 call 方法没有参数,或者参数为 null 或 undefined,则等同于指向全局对象。
如果 call 方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入 call 方法。
var f = function () {
return this;
};
f.call(5)
// Number {[[PrimitiveValue]]: 5}
上面代码中,call 的参数为 5,不是对象,会被自动转成包装对象(Number 的实例),绑定 f 内部的 this。
call 方法还可以接受多个参数。call 的第一个参数就是 this 所要指向的那个对象,后面的参数则是函数调用时所需的参数。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
上面代码中,call 方法指定函数 add 内部的 this 绑定当前环境(对象),并且参数为 1 和 2,因此函数 add 运行后得到 3。
call 方法的一个应用是调用对象的原生方法。
var obj = {};
obj.hasOwnProperty('toString') // false
// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty('toString') // true
Object.prototype.hasOwnProperty.call(obj, 'toString') // false
上面代码中,hasOwnProperty 是 obj 对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call 方法可以解决这个问题,它将 hasOwnProperty 方法的原始定义放到 obj 对象上执行,这样无论 obj 上有没有同名方法,都不会影响结果。
利用原生js实现call方法:
function call(context, ...params) {
context == undefined ? (context = window) : null;
// context不能是基本数据类型值,如果传递是值类型,我们需要把其变为对应类的对象类型
if (!/^(object|function)$/.test(typeof context)) {
if (/^(symbol|bigint)$/.test(typeof context)) {
context = Object(context);
} else {
context = new context.constructor(context);
}
}
let key = Symbol("context");
let result = null;
context[key] = this;
result = context[key](...params);
delete context[key];
return result;
};
apply()
apply 方法的作用与 call 方法类似,也是改变 this 指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
apply 方法的第一个参数也是 this 所要指向的那个对象,如果设为 null 或 undefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在 call 方法中必须一个个添加,但是在 apply 方法中,必须以数组形式添加。
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2```
-
找出数组最大元素,JavaScript 不提供找出数组最大元素的函数。结合使用 apply 方法和 Math.max 方法,就可以返回数组的最大元素。
var a = [10, 2, 4, 15, 9]; Math.max.apply(null, a) // 15
-
转换类似数组的对象,利用数组对象的 slice 方法,可以将一个类似数组的对象(比如 arguments 对象)转为真正的数组。
Array.prototype.slice.apply({0: 1, length: 1}) // [1] Array.prototype.slice.apply({0: 1}) // [] Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined] Array.prototype.slice.apply({length: 1}) // [undefined]
上面代码的 apply 方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有 length 属性,以及相对应的数字键。
-
绑定回调函数的对象
var o = new Object(); o.f = function () { console.log(this === o); } var f = function (){ o.f.apply(o); // 或者 o.f.call(o); }; // jQuery 的写法 \$('#button').on('click', f);
上面代码中,点击按钮以后,控制台将会显示 true。由于 apply 方法(或者 call 方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的 bind 方法。
bind()
bind 方法用于将函数体内的 this 绑定到某个对象,然后返回一个新函数。
var counter = {
count: -1,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1
上面代码中,counter.inc 方法被赋值给变量 func。这时必须用 bind 方法将 inc 内部的 this,绑定到 counter,否则就会出错。
this 绑定到其他对象也是可以的。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var obj = {
count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
上面代码中,bind 方法将 inc 方法内部的 this,绑定到 obj 对象。结果调用 func 函数以后,递增的就是 obj 内部的 count 属性。
bind 还可以接受更多的参数,将这些参数绑定原函数的参数。
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
newAdd(5) // 20
上面代码中,bind 方法除了绑定 this 对象,还将 add 函数的第一个参数 x 绑定成 5,然后返回一个新函数 newAdd,这个函数只要再接受一个参数 y 就能运行了。
如果 bind 方法的第一个参数是 null 或 undefined,等于将 this 绑定到全局对象,函数运行时 this 指向顶层对象(浏览器为 window)。
利用原生js实现bind:
function bind(context = window, ...params) {
let _this = this;
return function anonymous(...inners) {
_this.apply(context, params.concat(inners));
};
}
bind 方法有一些使用注意点。
-
每一次返回一个新函数,bind 方法每运行一次,就返回一个新函数,这会产生一些问题。
var obj = { init: 1, add: function(a, b) { return a + b + this.init; } } obj.add(1, 2); // 4 var plus = obj.add; plus(3, 4); // NaN,因为 this.init 不存在,这里的 this 指向 window/global plus.call(obj, 3, 4) // 8 plus.apply(obj, [3, 4]); // 8, apply 和 call 的区别就是第二个参数为数组 plus.bind(obj, 3, 4); // 返回一个函数,这里就是 bind 和 call/apply 的区别之一,bind 的时候不会立即执行 plus.bind(obj, 3, 4)(); // 8
-
结合回调函数使用,回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含 this 的方法直接当作回调函数。
var counter = { count: 0, inc: function () { 'use strict'; this.count++; } }; function callIt(callback) { callback(); } callIt(counter.inc.bind(counter)); counter.count // 1
上面代码中,callIt 方法会调用回调函数。这时如果直接把 counter.inc 传入,调用时 counter.inc 内部的 this 就会指向全局对象。使用 bind 方法将 counter.inc 绑定 counter 以后,就不会有这个问题,this 总是指向 counter。
-
还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的 this 指向,很可能也会出错。
var obj = { name: '张三', times: [1, 2, 3], print: function () { this.times.forEach(function (n) { console.log(this.name); }); } }; obj.print() // 没有任何输出
上面代码中,obj.print 内部 this.times 的 this 是指向 obj 的,这个没有问题。但是,forEach 方法的回调函数内部的 this.name 却是指向全局对象。解决这个问题,也是通过 bind 方法绑定 this。
obj.print = function () { this.times.forEach(function (n) { console.log(this.name); }.bind(this)); }; obj.print() // 张三 // 张三 // 张三