前置知识
- this 关键字
- js原型及原型链
背景
由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this
的指向是可变的。
this
的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。
有时,需要把this固定下来,避免出现意想不到的情况。如果对this
不太了解,可以先看一下我另一篇关于 this 的文章
概念
JavaScript 提供了
call、apply、bind
这三个方法,来切换/固定this的指向。
这三个方法都放在了Function对象
的原型对象上,因此所有Function实例对象上都能调用这三个方法,也就是说只有函数才能调用这三个方法。
Function.prototype.call()
函数实例的call
方法,可以指定函数内部this
的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
语法:func.call(thisValue, arg1, arg2, ...)
第一个参数是this所需要指向的那个对象;后面的参数是函数调用时所需的参数。
call
方法的第一个参数,应该是一个对象。如果参数为空、null、undefined
,则默认传入全局对象
var n = 123;
var obj = { n: 456; };function a() {console.log(this.a);
}a.call(); // 123
a.call(null); // 123
a.call(undefined); // 123
a.call(window); // 123
a.call(obj); // 123
如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call方法。
function f() {return this;
};f.call(5); // Number {[[PrimitiveValue]]: 5}
call
方法的其中一个使用场景是调用对象的原生方法。
看下面示例:
var obj = {};
obj.hasOwnProperty('toString') // false// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {return true;
};
obj.hasOwnProperty('toString') // trueObject.prototype.hasOwnProperty.call(obj, 'toString') // false
上面代码中,hasOwnProperty
是obj
对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。
call
方法可以解决这个问题,它将hasOwnProperty
方法的原始定义放到obj
对象上执行,这样无论obj
上有没有同名方法,都不会影响结果。
可能最后一行有点绕,这里再啰嗦解释一下:
因为obj
对象可能覆盖Object
原型上的方法
因此,这里我们可以直接执行原型的方法。然后将函数内部的指向改为obj
对象,并传入'toString'
参数,这时候其实就等同于上面代码第二行
因为拿第二行来说,我们知道通过obj
调用hasOwnProperty
,这时候在hasOwnProperty
函数内部,this
肯定就是obj
。
因此最后一行的执行结果和第二行是一样的。区别在于,最后一行是直接调用的原始定义方法,不会受后续重写方法的影响
Function.prototype.apply()
apply
方法的作用与call
方法类似,也是改变this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数。
语法: func.apply(thisValue, [arg1, arg2, ...])
Function.prototype.bind()
概念: bind()
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数。
语法: func.bind(thisValue, arg1, arg2, /* …, */ argN)
bind()
除了第一个绑定this
参数之外,还可以接受更多的参数,将这些参数绑定原函数的参数。
例如func
方法有3个参数,那么我们在绑定的时候可以预先传入几个参数,然后我们在后面实际调用新函数的时候,只需要传剩余参数即可。如下所示。
function func(x, y, z) {console.log(this.name, x, y, z);
}var obj = {name: '张三',
};var newFunc = func.bind(obj, 100);
newFunc(200, 300) // 张三 100 200 300
再来看下面这个例子
我们将d.getTime()
方法赋给变量print
,然后调用print()
就报错了。
这是因为getTime()
方法内部的this
,绑定Date
对象的实例,赋给变量print
以后,内部的this
已经不指向Date
对象的实例了。
var d = new Date();
d.getTime() // 1481869925657var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
bind()
方法可以解决这个问题。
var print = d.getTime.bind(d);
print() // 1481869925657
上面代码中,bind()
方法将getTime()
方法内部的this
绑定到d
对象,这时就可以安全地将这个方法赋值给其他变量了。
再来看一个使人困惑的例子
利用bind()
方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的slice()
方法为例。
var newSlice = Function.prototype.call.bind(Array.prototype.slice);
newSlice([1, 2, 3], 0, 1) // [1]
这段代码可能理解起来有点困难,又是call
又是bind
的,我们需要拆分出来理解。
首先,这行代码是直接调用了函数原型上的call
函数。
然后我们再通过call函数(Function.prototype.call
可以看成call
) 调用一个bind
方法来生成一个新的函数。
这里先将Array.prototype.slice
看做是一个对象
然后通过调用bind方法将Array.prototype.slice
变成Function.prototype.call
方法所在的对象。
如果对上面这句话感到有点困惑的话,看这里~
首先我们来回顾一下
bind
方法的语法和概念
概念:bind()
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数。
语法:func.bind(thisValue)
通过对概念的理解,我们可以对语法做一个推算:
func.bind(thisValue)
就相当于thisValue.func
其实这就是绑定this的原理: 将func
变为了thisValue
对象上的一个方法了。这时候func
里的this
指向,肯定就是指向thisValue
对象了。
这只再回头看一下上面那句话,是否会清晰一下
上面看懂之后,调用时就变成了Array.prototype.slice.call
示例就可以写成下面这样
Function.prototype.call.bind(Array.prototype.slice)
// 等同于
Array.prototype.slice.call()
var slice = Function.prototype.call.bind(Array.prototype.slice);
// 这时候 slice 就相当于 Array.prototype.slice.call()
// 因此
slice([1, 2, 3], 0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
// 等同于
[1, 2, 3].slice(0, 1) // [1]
call/apply 和 bind的区别
call/apply
是立即执行,bind
是返回一个函数,后期需要的时候执行。
bind
可以先传入部分参数,在执行的时候传入剩余参数;
call/apply
因为是立即执行,所以要在调用的时候传入所有参数
参考链接
阮一峰 js教程
MDN官网