-
this指向
this
定义-
this
的两种绑定方式- 默认绑定
-
显式绑定
new
绑定(具有显式绑定效果)- 隐式绑定(具有显式绑定效果)
this
绑定优先级- 函数的
this
指向 this
绑定丢失的情况- 手写
call
、apply
和bind
this指向
this
定义
this
用于指定对当前对象的引用。
this
的两种绑定方式
为什么说是两种?在《你不知道的JavaScript(上卷)》一书中共提到了四种绑定方式。如下:
-
默认绑定
-
隐式绑定
-
显式绑定
-
new
绑定
实际上这四种绑定方式有两种方式重复了(隐式绑定和new
绑定)。我们在学习过程中应带有辩证思维去看待问题。基于獭子细致的分析与总结,实际上this
的绑定可以认为只存在两种方式:默认绑定和显式绑定。分析如下:
默认绑定
在严格模式下,全局作用域的this
对象会变为undefined
。在非严格模式的普通函数若不作为对象方法,它的this
绑定都会自动绑定到window
全局对象上,即默认绑定。
显式绑定
函数可以使用call
/apply
/bind
等方法绑定this
对象。这种方式属于强制绑定措施,this
的指向是可预见的(即绑定谁就指向谁),我们重点谈谈他们的用法。
基本格式:函数名称.call(要绑定的对象, 参数列表)
call
:接受一个参数列表。会立即执行。
apply
:接受数组形式的参数。会立即执行。
bind
:接受一个参数列表。返回原函数拷贝,不会立即执行。
若需应用,一般可以这样思考:我们想要函数的this
值指向哪个对象?
let a = { name: '小红' }
function getName() {
console.log(this.name)
}
getName() // 这里默认绑定全局对象
getName.call(a) // 显式绑定对象a
new
绑定(具有显式绑定效果)
我们来看看new
关键字的执行过程:
- 创建一个新的空对象
- 将构造函数的原型赋给新创建对象(实例)的隐式原型
- 利用显式绑定将构造函数的
this
绑定到新创建对象并为其添加属性 - 返回这个对象
很显然,这里的this
的指向同样是可预见的。
基于上面的执行过程,我们可以手写实现一下(面试题):
function myNew(fn, ...args) { // 构造函数作为参数
let obj = {}
obj.__proto__ = fn.prototype
fn.apply(obj, args)
return obj
}
一步一行代码,是不是很简洁。
注意:我们可以理解为new
的过程应用了显式绑定
隐式绑定(具有显式绑定效果)
关于隐式绑定,这里提一下书里被翻译过的作者原话:
另一条需要考虑的规则是调用位置是否有上下文对象
其实隐式绑定也可以理解为它应用了显式绑定。比如我们在利用模板字面量创建对象的时候,普通函数作为对象方法拥有与显式绑定同样的效果,可以理解为已经执行了显式绑定这一过程。即函数会绑定对应的实例对象。如下:
let a = {
x: 10,
y: function () { // 作为对象方法,存在显示绑定效果。
console.log(this)
}
}
a.y()
// 输出结果:a { x: 10 y: f } 即函数内部的this指向被绑定的实例对象
this
绑定优先级
顺序:显式绑定 > 默认绑定
注意:箭头函数本身没有this
,不会应用以上规则
函数的this
指向
关于this
指向,我们会更多的关注函数内部的this
指向。一般而言会考察以下两种类型的题目:
- 自定义对象内部函数的
this
指向 - 全局对象下函数的
this
指向
可以利用以下准则去解决this
指向问题。实际上我们只需处理这两种函数:
-
对于非严格模式下的普通函数会有两个情况:
1.1 作为对象方法,
this
会绑定对象(执行new绑定过程)。1.2 不作为对象方法,在非严格模式下
this
默认绑定window
。 -
箭头函数没有
this
。它只会继承最近一层普通函数或全局作用域的this
。
注意:call
/apply
/bind
方法不能改变箭头函数的this
指向,因为箭头函数本身没有this
。
我们尽量一次性解决所有this
指向问题。首先设计这样两个结构,如下:
// 普通函数结构
function a(){} // 普通函数声明
setTimeout(function(){})// 内置函数
(function(){})() // 立即执行函数
return function(){} // 匿名函数
// 箭头函数结构
let a = ()=>{} // 箭头函数声明
setTimeout(()=>{}) // 内置函数
(()=>{})() // 立即执行函数
return ()=>{} // 匿名函数
利用上面的结构。分析第一种情况。如下:
let obj = {
fun: function () { // 这里是普通函数
console.log(this) // 普通作为对象方法定义。内部this指向obj
// 以下普通函数都不作为对象方法定义,this全部指向window
function a() { console.log(this) }; a(); // 普通函数声明执行
setTimeout(function () { console.log(this) });// 内置函数
(function () { console.log(this) })(); // 立即执行函数
return function () { console.log(this) }; // 匿名函数
},
arr: () => { // 这里是箭头函数
console.log(this) // 箭头函数的this俺规则继承全局作用域指向window
// 以下普通函数都不作为对象方法定义,this全部指向window
function a() { console.log(this) }; a(); // 普通函数声明执行
setTimeout(function () { console.log(this) });// 内置函数
(function () { console.log(this) })(); // 立即执行函数
return function () { console.log(this) }; // 匿名函数
}
}
obj.fun()() // 会执行普通函数内部的所有普通函数和返回的匿名函数
obj.arr()() // 会执行箭头函数内部的所有普通函数和返回的匿名函数
输出结果:第一个this
为obj
对象,后面九个this
全是window
对象
接下来分析第二种情况。如下:
let obj = {
fun: function () { // 这里是普通函数
console.log(this) // 普通作为对象方法定义。this指向obj
// 以下是箭头函数,它的this按规则继承最近一次普通函数即全部指向obj
let a = () => { console.log(this) }; a();// 箭头函数声明
setTimeout(() => { console.log(this) }); // 内置函数
(() => { console.log(this) })(); // 立即执行函数
return () => { console.log(this) }; // 匿名函数
},
arr: () => { // 这里是箭头函数
console.log(this) // 箭头函数的this俺规则继承全局作用域指向window
// 以下是都是箭头函数,它的this按规则继承全局作用域全部指向window
let a = () => { console.log(this) }; a();// 箭头函数声明
setTimeout(() => { console.log(this) }); // 内置函数
(() => { console.log(this) })(); // 立即执行函数
return () => { console.log(this) }; // 匿名函数
}
}
obj.fun()() // 会执行普通函数内部的所有箭头函数和返回的匿名函数
obj.arr()() // 会执行箭头函数内部的所有箭头函数和返回的匿名函数
输出结果:前五个this
都是obj
对象,后面五个this
都是window
对象
分析第三种情况。如下:
// 箭头函数和普通函数放在全局中声明,全部指向window
function fun() { console.log(this) }; fun(); // 普通函数声明执行
setTimeout(function () { console.log(this) });// 内置函数
(function () { console.log(this) })(); // 立即执行函数
let arr = () => { console.log(this) }; arr(); // 箭头函数声明
setTimeout(() => { console.log(this) }); // 内置函数
(() => { console.log(this) })(); // 立即执行函数
输出结果:六个this
全是window
对象
总结解题的关键点:首先判断是箭头函数还是普通函数。箭头函数只会按规则继承this
指向。普通函数则要分两种情况:作为对象方法和不作为对象方法:作为对象方法this
会绑定到对象上,不作为对象方法this
则绑定到window
(非严格模式)。
补充说明:普通函数作为对象方法实际上已经执行了new
绑定(可以看上面的手写new过程)。因此普通函数作为对象方法,它的this
会指向对象。
this
绑定丢失的情况
函数别名(作为参数被传递或调用):例如obj.foo
或手写Promise中的resolve
方法。我们可以理解为他是一个已经定义好的函数。它的this指向具体要看他在哪里使用,而且要分清楚它是普通函数还是箭头函数。
obj.foo // 是一个函数
// 等价于下面我们定义好的普通函数或箭头,如下
let foo = function{}{ console.log(this) }
let foo = ()=>{ console.log(this) }
补充说明:实际上this丢失只有这一种情况
手写call
、apply
和bind
前置知识:ES6 剩余参数、Function.prototype
原型方法定义,在调用时每个function
可通过隐式原型(原型链)找到此方法。
Function.prototype.myCall = function (obj, ...args) {
obj = obj === null || obj === undefined ? window : obj;
return (() => {
obj.method = this; //作为临时方法传递给对象
obj.method(...args);
delete obj.method;
})();
}
call
和apply
区别,apply
的接受参数为数组形式。
Function.prototype.myApply = function (obj, ...args) {
obj = obj === null || obj === undefined ? window : obj;
return (() => {
obj.method = this; //作为临时方法传递给对象
obj.method(...args[0]);
delete obj.method;
})();
}
普通版:bind
方法是硬绑定。返回值为原函数的拷贝,其this
值不可再修改。
Function.prototype.myBind = function (obj, ...args1) {
obj = obj === null || obj === undefined ? window : obj;
return (...args2) => {
this.apply(obj, args1.concat(args2));
};
}
进阶版:bind
方法可支持new
关键字
Function.prototype.myNewBind = function (obj, ...args1) { // 函数 1
obj = obj === null || obj === undefined ? window : obj;
let self = this;
let fn = function (...args2) { // 函数 2
return self.apply(this instanceof fn ? this : obj, args1.concat(args2));
};
fn.prototype = Object.create(self.prototype); // 维持其原型
fn.prototype.constructor = fn
return fn;
}
过程解析:
- 为什么要维持原形?
原生函数中bind
在执行new
操作时会保留其所绑定函数的原型,我们希望在执行new
关键字后myNewBind
函数也能拥有同样的效果。若没有进行维持原型这一步操作,我们的new
操作效果其实是把返回的函数 2 作为构造函数操作去生成实例,会丢失之前所绑定函数的原型,无法实现继承。
- 为什么使用
instanceof
?
判断当前对象是否为返回的构造函数所生成的实例对象。若是则认为执行了new
关键字操作,返回的构造函数内部需要this
代表新的实例对象,而不是旧的obj
对象。
- 为什么要使用
Object.create()
?
我们希望返回的函数也有自己的独立原型。直接将一个构造函数原型赋给另一个构造函数原型会使两个原型对象的数据捆绑(引用值特点)在一起,即需要保持原型对象数据的独立性。
Object.create()
的运行过程手写如下:
function createObject(obj) { // 参数为原型对象
let temp = function () { };
temp.prototype = obj;
return new temp();
}
参考
你不知道的JavaScript (上卷)