一文搞定this、apply、call、bind
背景
JavaScript
中最容易被误解的一个方面是this
关键字。看我本篇文章,它们将不会是问题。
this的指向
在 ES5
中,「this 永远指向最后调用它的那个对象」。
下面我们来看一个最简单的例子:
var name = "windowsName"; function a() { var name = "Cherry"; console.log(this.name); // windowsName console.log("inner:" + this); // inner: Window } a(); console.log("outer:" + this) // outer: Window
我们看最后调用 a
的地方 a();
,前面没有调用的对象那么就是全局对象 window,这就相当于是 window.a()
;注意,这里我们没有使用严格模式,如果使用严格模式的话,全局对象就是 undefined
,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined
。
再看下这个例子:
var name = "windowsName"; var a = { name: "Cherry", fn : function () { console.log(this.name); // Cherry } } a.fn();
在这个例子中,函数 fn
是对象 a
调用的,所以打印的值就是 a
中的 name
的值。
再来看一个比较坑的例子:
var name = "windowsName"; var a = { name : null, // name: "Cherry", fn : function () { console.log(this.name); // windowsName } } var f = a.fn; f();
这里你可能会有疑问,为什么不是 Cherry
,这是因为虽然将 a
对象的 fn
方法赋值给变量f
了,但是没有调用,「this
永远指向最后调用它的那个对象」,由于刚刚的 f
并没有调用,所以 fn()
最后仍然是被window
调用的。所以 this
指向的也就是 window
。
由以上五个例子我们可以看出,this
的指向并不是在创建的时候就可以确定的,在 ES5
中,「this
永远指向最后调用它的那个对象」。
怎么改变 this 的指向
我们看个栗子:
var name = '夏安'; var obj = { name: '...夏安', print: function () { setTimeout(function() { console.log(this.name); }, 0); } } obj.print(); // undefined
实际上调用匿名函数的是Timeout
对象,所以this
指的是Timeout
对象,因为Timeout
对象没有name
属性,所以输出undefined
。那么如果我们想让this
指向obj
,我们该怎么操作呢?
箭头函数
众所周知,ES6
的箭头函数是可以避免 ES5
中使用 this
的坑的。「箭头函数的 this
始终指向函数定义时的 this
,而非执行时。」,箭头函数需要记着这句话:“箭头函数中没有 this
绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this
绑定的是最近一层非箭头函数的 this
,否则,this
为 undefined
”。
var name = '夏安'; var obj = { name: '...夏安', print: function () { setTimeout(() => { console.log(this.name); }, 0); } } obj.print(); // ...夏安
在函数内部使用 _this = this
var name = '夏安'; var obj = { name: '...夏安', print: function () { var _this = this; setTimeout(function() { console.log(_this.name); }, 0); } } obj.print(); // ...夏安
在 print
中,首先设置 var _this = this;
,这里的 this
是调用 print
的对象 obj
,我们将 this(指向变量 obj)
赋值给一个变量 _this
,这样,在 「匿名函数」 中我们使用 _this
就是指向对象 obj
了。
使用 call
call
是每个函数的一个方法,它允许你调用函数,指定调用函数的上下文。语法如下:
fun.call(thisArg[, arg1[, arg2[, ...]]])
call
接收多个参数,第一个为函数上下文也就是this
,后边参数为函数本身的参数。
var name = '夏安'; var obj = { name: '...夏安', print: function (name) { console.log(this.name); console.log(name); } } let { print } = obj; print.call(obj, '夏安...'); // ...夏安 // 夏安...
使用 apply
其实 apply
和 call
基本类似,他们的区别只是传入的参数不同。apply
和 call
的区别是 call
方法接受的是若干个参数列表,而 apply
接收的是一个包含多个参数的数组。
var name = '夏安'; var obj = { name: '...夏安', print: function (name) { console.log(this.name); console.log(name); } } let { print } = obj; print.apply(obj, ['夏安...']); // ...夏安 // 夏安...
使用 bind
bind()
方法创建一个新的函数, 当被调用时,将其this
关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
所以我们可以看出,bind
是创建一个新的函数,我们必须要手动去调用:
var name = '夏安'; var obj = { name: '...夏安', print: function (name) { console.log(this.name); console.log(name); } } let { print } = obj; print.bind(obj, '夏安...')(); // ...夏安 // 夏安...
手写 call
function mySymbol(obj) { let unique = (Math.random() + new Date().getTime()).toString(32).slice(0, 8); if (obj.hasOwnProperty(unique)) { return mySymbol(obj); //递归调用 } else { return unique; } } Function.prototype.myCall = function (context) { // 如果没有传或传的值为空对象 context指向window context = context || window; let fn = mySymbol(context); context[fn] = this; //给context添加一个方法 指向this // 处理参数 去除第一个参数this 其它传入fn函数 let arg = [...arguments].slice(1); //[...xxx]把类数组变成数组 context[fn](...arg); //执行fn delete context[fn]; //删除方法 } let Person = { name: '夏安...', say(age) { console.log(this); console.log(`我叫${this.name}我今年${age}`); } } Person1 = { name: '夏安' } Person.say.myCall(Person1, 18); //我叫夏安我今年18
手写 apply
function mySymbol(obj) { let unique = (Math.random() + new Date().getTime()).toString(32).slice(0, 8); if (obj.hasOwnProperty(unique)) { return mySymbol(obj); //递归调用 } else { return unique; } } Function.prototype.myApply = function (context) { // 如果没有传或传的值为空对象 context指向window context = context || window; let fn = mySymbol(context); context[fn] = this; //给context添加一个方法 指向this // 处理参数 去除第一个参数this 其它传入fn函数 let arg = [...arguments].slice(1); //[...xxx]把类数组变成数组 context[fn](...arg[0]); //执行fn delete context[fn]; //删除方法 } let Person = { name: '夏安...', say(age) { console.log(this); console.log(`我叫${this.name}我今年${age}`); } } Person1 = { name: '夏安' } Person.say.myApply(Person1, [18]); //我叫夏安我今年18
手写 bind
Function.prototype.myBind = function (context) { //返回一个绑定this的函数,我们需要在此保存this let self = this // 可以支持柯里化传参,保存参数 let arg = [...arguments].slice(1) // 返回一个函数 return function () { //同样因为支持柯里化形式传参我们需要再次获取存储参数 let newArg = [...arguments] console.log(newArg) // 返回函数绑定this,传入两次保存的参数 //考虑返回函数有返回值做了return return self.apply(context, arg.concat(newArg)) } } let Person = { name: '夏安...', say(age) { console.log(this); console.log(`我叫${this.name}我今年${age}`); } } Person1 = { name: '夏安' } Person.say.myBind(Person1, 18)(); //我叫夏安我今年18