说起 JavaScript 中的 this ,总是让我晕头转向。各种有关 this 指向的笔试题,看了不少,但遇见还是会做错,感觉还是没有完全理解其中的奥妙。所以去看了一下《你不知道的JavaScript 上卷》,特此将学习的有关this的知识记录如下。
一、什么是 this ?
1.1 常见理解误区
- 理解误区1:this 是指向函数本身的引用
this 翻译过来是“这个”,顾名思义,这个啥呢。很多人可能就把 this 理解为指向函数自身的引用了,当然我自己也是一直这么认为的。但事实并非如此,我们看下面这个例子:
function foo(i) {
console.log(i);
this.count ++;
}
foo.count = 0;
for (let i = 0; i < 3; i++) {
foo(i); // 0 1 2
}
console.log(foo.count); // 0
如上代码,首先通过foo.count = 0给函数对象foo添加了count属性,并初始化为0。然后foo函数执行了3次。如果 this 是指向函数自身的引用的话,那么console.log(foo.count);输出的值应该是3,但实际上输出的是0。
function foo(i) {
console.log(i);
foo.count ++;
}
foo.count = 0;
for (let i = 0; i < 3; i++) {
foo(i); // 0 1 2
}
console.log(foo.count); // 3
再看这个段代码,将this.count++改成foo.count ++;,最终console.log(foo.count);输出的值是3。这也说明了,其实this并非指向函数本身。
- 理解误区2:this 指向函数的作用域
首先,函数的作用域是一套标识符访问的规则,存在于 JavaScript 引擎内部,是无法通过 JavaScript 代码访问的。所以,无论任何情况下,this 都不可能执行函数的作用域的。
此外,我们再看下面的例子:
function foo() {
var a = 2;
console.log(this.a);
}
foo(); // undefined
如果 this 是指向函数的作用域,那么foo函数的作用域中声明并初始化了变量a,但是最终输出的 this.a值为undefined。所以,this并非指向函数的作用域。
1.2 this到底是啥?
解决了上述两个理解误差,那 this 的庐山真面目到底是啥呢?
this 实际上是在函数调用时发生的绑定,它的指向取决于函数在哪里被调用。
JavaScript中每个函数被调用时都会创建一个执行上下文,而执行上下文中一个组件就是this绑定(thisBinding),即该执行上下文中this关键字关联的值。有关这方面的值可以了解一下词法环境和执行上下文。
二、this 绑定的对象
2.1 this 绑定规则 和 优先级
首先按《你不知道的JavaScript上卷》中对this的解析,介绍一下this绑定的规则。
(1)默认绑定
函数独立调用时,即为默认规则:
- 非严格模式下:this 指向全局对象
- 严格模式下:this 绑定到
undefined那什么是函数独立调用呢,即类似于foo()的形式,我们看如下代码:
// 非严格模式下
function foo() {
console.log(this);
}
window.foo(); // Window(浏览器环境下的全局对象)
// 严格模式下
function foo() {
"use strict"
console.log(this);
}
window.foo(); // undefined
(2)隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
此外,对象引用链中只有最顶层(最后一层)会影响调用位置
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};f
obj1.obj2.foo(); // 42
再来看一个例子,这里var bar = obj.foo;只是把对象 obj 中的属性 foo 的值赋给了 bar,所有执行bar()的时候,并非通过obj来调用foo函数的,而实独立调用的。所以非严格模式下,this默认绑定全局对象,输出的this.a即为"oops, global"
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo;
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
再来看个回调函数的例子:最后一行代码中obj.foo其实是当作一个参数传递给setTimeout函数的,当倒计时(1ms)结束后,再来调用foo函数时,其实是一个独立调用。所以非严格模式下,this默认绑定全局对象,输出的this.name即为"Window"
function foo() {
console.log(this.name);
}
obj = {
name: "obj",
foo: foo
}
var name = "window";
setTimeout(obj.foo, 1);
最后我们看个数组的例子,这是一个比较容易混淆的点。下面的代码中,最后调用arr[0]()的结果是1,这是因为数组本质上是一个对象,fn是数组的元素,也可以看成对象arr属性0的值,所以当调用arr[0]()时,函数中的this其实是指向数组arr的,arr的length属性值为1,所以最终输出的是1.
var len = 10;
function fn() {
console.log(this.len);
}
let arr = [fn];
arr[0](); // 1
(3)显示绑定
通过 call()、apply()、bind() 显示绑定this,后续章节详细介绍。
(4)new 绑定
new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例 —— MDN
与其他语言中的new不同,JavaScript中的new用于调用构造函数,反之JavaScript中的构造函数是使用new运算符时被调用的函数。
new 关键字会进行如下的操作:
- 创建一个空的简单JavaScript对象(即
{}); - 为步骤1新创建的对象添加属性
__proto__,将该属性链接至构造函数的原型对象 ; - 将步骤1新创建的对象作为
this的上下文 ; - 如果该函数没有返回对象,则返回
this。
注意第3步,函数调用的this绑定到这个创建的对象:
function foo() {
this.name = "haha";
console.log(this);
}
let obj = new foo(); // foo {name: 'haha'}
console.log(obj.name) / haha
优先级
说了以上四种规则,那么当这些规则同时存在时优先级如何呢?
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
可以按照下面的顺序来进行判断this指向:
- 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
- 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。
var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
var bar = foo()
2.2 箭头函数的this
首先来看一下MDN中对于箭头函数的定义:
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的 this 、 arguments 、 super 、 new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。 —— MDN
从定义中可知,箭头函数并没有自己的this,所以并不满足2.1中的 this 绑定规则,而是根据外层作用域来决定this的。且箭头函数的绑定无法被修改!
例1:
function callback() {
console.log(this.a);
}
function foo() {
setTimeout(callback ,0);
console.log(this) // {a: 2}
}
function arrowFoo() {
setTimeout(() => {
console.log( this.a );
},100);
console.log(this); // {a: 2}
}
var obj = {
a:2
};
var a = 1;
foo.call( obj ); // 1
arrowFoo.call( obj ); // 2
如上述代码所示:
- 首先对
foo.call( obj );进行分析,通过call将foo的this绑定到obj, 所以输出的this值为{a: 2}。但是callback()仍是独立调用,所以默认绑定全局对象,输出1. - 再对
arrowFoo.call( obj )分析,同样通过call将arrowFoo的this绑定到obj, 所以输出的this值为{a: 2}。但是setTimeout()中的回调函数是箭头函数,所以会捕获到调用时arrowFoo的this。所以输出的结果为2。
例2:
// 返回一个箭头函数
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
上述例子很好的说明了箭头函数的this绑定无法修改。var bar = foo.call( obj1 )执行完毕后,已经将箭头函数的this绑定了obj1,虽然后面通过call显示绑定obj2,但是箭头函数的this绑定不能修改,所以最终结果输出的是2.
之前网上看到一张图,可以大致总结上述有关this绑定的规则了:
2.3 从执行上下文的角度来看 this
看了很多有关this的文章,大部分都是基于以上规则,分成不同的情况去介绍的。但是自从之前看了有关词法环境和执行上下文的知识后,个人感觉从这个角度去研究this一波。
上述也说道,this是函数调用时发生的绑定。既然有函数调用,那么与执行上下文必定有关。官方文档中写道:
当控制流根据一个函数对象F、调用者提供的
thisArg和argumentList,进入函数代码的执行上下文时,会执行以下步骤:
this 绑定
- 如果函数代码是严格模式下的代码,直接 this 绑定为 thisArg。
- 如果函数代码是非严格模式下的代码:
- thisArg 为
undefined或null,则设 this 绑定为全局对象。- 否则,如果
Type(thisArg)的结果不为Object,则设 this 绑定 为 >ToObject(thisArg)- 否则,this 绑定为 thisArg
所以函数的this绑定是什么,主要还是要看调用者提供的thisArg。
// 严格模式
function foo() {
"use strict"
console.log(this);
}
var obj = {}
foo.call(undefined); // undefined
foo.call(null); // null
foo.call(1); // 1
foo.call(obj); // {}
// 非严格模式下
function foo() {
console.log(this);
}
var obj = {}
foo.call(undefined); // Window
foo.call(null); // Window
foo.call(1); // Number {1}
foo.call(obj); // {}
三、call()、apply()、bind()
第二节提到的显示绑定主要和call()、apply()、bind()三个函数相关,平常很多地方也会用到,这里也详细介绍一下这三个函数。
3.1 Function.prototype.call()
call 方法可以使用一个指定的 this 值和单独给出一个或多个参数来调用一个函数。
(1)语法
function.call(thisArg, arg1, arg2, ...)
(2)参数
thisArg:可选,在 function 函数运行时使用的this值。(若该参数未指定,非严格模式下:this值会被绑定为全局对象,严格模式下,this的值会被绑定为全局对象)arg1, arg2 ...:指定的参数列表,即传入 function 函数的参数
(3)返回值
使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
(4)手动实现
// 注意:这是非严格模式下call的实现
Function.prototype._call = function(thisArg, ...args) {
// 若 thisArg 为 undefined 或者 null,thisArg 设置为全局对象
// Object(thisArg):转换为对象
thisArg = thisArg != undefined && thisArg != null ? Object(thisArg) : globalThis;
// 给 thisArg 添加一个唯一的符号属性
let key = Symbol('key');
// this 为调用 _call 的函数对象
thisArg[key] = this;
// 隐式绑定 thisArg
const result = thisArg[key](...args);
// 删除新增的属性
delete thisArg[key];
// 返回调用的结果
return result;
}
(5)call 的应用场景
- 使用 call 方法调用父构造函数
- 使用 call 方法调用匿名函数
- 使用 call 方法调用函数并指定上下文的
this
3.2 Function.prototype.apply()
apply() 方法调用一个具有给定this值的函数,以及以一个数组(或 类数组对象)的形式提供的参数
(1)语法
function.apply(thisArg, [argsArray])
(2)参数
thisArg:必选,在 function 函数运行时使用的this值。argsArray:可选,一个数组或者类数组对象,其中的数组元素将作为单独的参数传给function函数。如果该参数的值为null或undefined,则表示不需要传入任何参数。- 数组字面量:[arg1, arg2, ...]
- 数组对象:new Array(arg1, arg2, ...)
- arguments对象:即拥有length属性、可以索引元素的类数组对象
- 类数组对象: {length: n},即带有length属性的对象
(3)返回值
调用有指定this值和参数的函数的结果。
(4)手动实现
Function.prototype._apply = function(thisArg, args) {
// 若 thisArg 为 undefined 或者 null,thisArg 设置为全局对象
// Object(thisArg):转换为对象
thisArg = thisArg != undefined && thisArg != null ? Object(thisArg) : globalThis;
// 给 thisArg 添加一个唯一的符号属性
let key = Symbol('key');
// this 为调用 _call 的函数对象
thisArg[key] = this;
// 隐式绑定 thisArg
// 若 args 为 undefined 或者 null 时,表示不传入任何参数
const result = args ? thisArg[key](...args) : thisArg[key]();
// 删除新增的属性
delete thisArg[key];
// 返回调用的结果
return result;
}
(5)apply 的应用场景
- 获取数组中的最大值和最小值
numbers = [1, 2, 3]
// 最大值
Math.max.apply(null, numbers);
// 最小值
Math.min.apply(null, numbers);
- 使用 apply 链接构造器
Function.prototype.construct = function (aArgs) {
var oNew = Object.create(this.prototype);
this.apply(oNew, aArgs);
return oNew;
};
3.3 Function.prototype.bind()
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
(1)语法
function.bind(thisArg[, arg1[, arg2[, ...]])
(2)参数
thisArg:调用绑定函数时作为this参数传递给目标函数的值.- 如果使用
new运算符构造绑定函数,则忽略该值(new 的优先级高于显示绑定) - 当使用
bind在setTimeout中创建一个函数(作为回调提供)时,作为thisArg传递的任何原始值都将转换为object。 - 如果
bind函数的参数列表为空,或者 thisArg 的值为undefined或者null,执行作用域的this将被视为新函数的thisArg.
- 如果使用
arg1, arg2, ...:当目标函数被调用时,被预置入绑定函数的参数列表中的参数
(3)返回值
返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
(4)具体描述
bind() 函数会创建一个新的绑定函数(bound function,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。
绑定函数具有以下内部属性:
- [[BoundTargetFunction]] - 包装的函数对象
- [[BoundThis]] - 在调用包装函数时始终作为 this 值传递的值。
- [[BoundArguments]] - 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。
- [[Call]] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表。
当调用绑定函数时,它调用 [[BoundTargetFunction]] 上的内部方法 [[Call]] ,就像这样 Call(boundThis, args) 。其中,boundThis 是 [[BoundThis]] ,args 是 [[BoundArguments]] 加上通过函数调用传入的参数列表。
(5) 手动实现
Function.prototype._bind = function(thisArg) {
// 当前调用 bind 的函数
let self = this;
// 获取所有其他参数
let args = Array.prototype.slice.call(arguments, 1);
// 接收函数原型
let fNOP = function() {};
// 绑定后的函数
let fBound = function() {
return self.apply(
this instanceof fNOP ? this : thisArg,
args.concat(Array.prototype.slice.call(arguments))
);
};
// 维护原型关系
if (this.prototype) {
fNOP.prototype = this.prototype;
}
// 通过 fNOP 传递函数原型给 fBound ,能够防止修改 fBound 的 prototype 时影响到 self 的 prototype
fBound.prototype = new fNOP();
return fBound;
}
有关bind的手动实现,看了很多博客,发现这一版写的比较详细一些。但是感觉仍然有缺陷,例如:bind 函数的参数列表为空,或者 thisArg 的值为 undefined 或者 null,执行作用域的this 将被视为新函数的 thisArg并未考虑到。以及绑定函数包装了原函数等等
以上就是个人学习this以及call apply bind 的一些小记录,希望各位大佬能指出有错误的地方。
主要参考:
[1] 《你不知道的JavaScript 上卷》
[2] MDN