发布于 2024 年 2 月 26 日,星期一
该JavaScript中`this`关键字的深层含义,通过逐步深入的方式揭示其背后的机制。`this`在JavaScript中并非固定指向某个对象,而是取决于函数的调用方式。在不同上下文中,如普通函数、箭头函数、对象方法或构造函数中,`this`的指向会有所不同。理解`this`的关键在于掌握其动态绑定特性,即在函数执行时才确定其指向的对象。深入剖析`this`的本质有助于开发者避免常见的陷阱,写出更健壮的代码。
系列首发于公众号『非同质前端札记』https://mp.weixin.qq.com/s?__biz=MzkyOTI2MzE0MQ==&mid=2247485576&idx=1&sn=5ddfe93f427f05f5d126dead859d0dc8&chksm=c20d73c2f57afad4bbea380dfa1bcc15367a4cc06bf5dd0603100e8bd7bb317009fa65442cdb&token=1071012447&lang=zh_CN#rd ,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。
调用位置
:调用位置就是函数在代码中被调用的位置(而不是声明的位置)
。独立函数调用
。可把这条规则看到是无法应用其他规则时的默认规则。function foo(){ console.log(this.a);}var a = 2;foo(); // 2
如果使用严格模式(strict mode),那全局对象将无法使用默认绑定,因此 this 会绑定到 undefined。
function foo(){ "use strict"; console.log(this.a);}var a = 2;foo(); // Type: this is undefined
只有 foo() 运行在非 strict mode下时,默认绑定才能绑定到全局对象;
严格模式下与 foo() 的调用位置无关。function foo(){ console.log(this.a);}var a = 2;(function (){ "use strict"; foo(); // 2})
strict mode
与 non-strict mode
,尽量减少在代码中混合使用 strict mode 和 non-strict mode。另一条规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或包裹。
function foo() { console.log(this.a); // 2}var obj = { a: 2, foo: foo}obj.foo();
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象上
,因此在调用 foo() 时 this 被绑定到了 obj 上,所以 this.a 与 obj.a 是一样的。对象属性引用链中只有最顶层或最后一层会影响调用位置
。function foo() { console.log( this.a );}var obj2 = { a: 42, foo: foo};var obj1 = { a: 2, obj2: obj2};obj1.obj2.foo(); // 42
function foo() { console.log( this.a );}var obj = { a: 2, foo: foo};var bar = obj.foo; // 函数别名!var a = "oops, global"; // a 是全局对象的属性bar(); // "oops, global"
function foo() { console.log( this.a );}function doFoo(fn) { // fn其实引用的是 foo fn(); // <-- 调用位置!}var obj = { a: 2, foo: foo};var a = "oops, global"; // a 是全局对象的属性doFoo( obj.foo ); // "oops, global"
function foo() { console.log( this.a );}var obj = { a: 2, foo: foo};var a = "oops, global"; // a 是全局对象的属性setTimeout( obj.foo, 100 ); // "oops, global"
call() 和 apply()。第一个参数是一个对象,也就是需要绑定的对象,第二个参数传入的参数,而两者之间的区别就在于第二个参数,call 的第二个参数是一个个参数,而 apply 则是一个参数数组。
// call()function foo() { console.log( this.a );}var obj = { a:2};foo.call( obj ); // 2// apply()function foo(something) { console.log( this.a, something ); return this.a + something;}var obj = { a:2};var bar = function() { return foo.apply( obj, arguments );};var b = bar( 3 ); // 2 3console.log( b ); // 5
let something = new MyClass();
。__proto__
(隐式原型) 等于函数的 prototype(显式原型)function foo() { console.log( this.a );}var obj1 = { a: 2, foo: foo};var obj2 = { a: 3, foo: foo};// 隐式绑定obj1.foo(); // 2obj2.foo(); // 3// 显式绑定obj1.foo.call( obj2 ); // 3obj2.foo.call( obj1 ); // 2
显式绑定的优先级更高
,也就是说在判断时应当考虑是否可以应用显式绑定。function foo(something) { this.a = something;}var obj1 = { foo: foo};var obj2 = {};// 隐式绑定obj1.foo( 2 );console.log( obj1.a ); // 2obj1.foo.call( obj2, 3 );console.log( obj2.a ); // 3// new绑定var bar = new obj1.foo( 4 );console.log( obj1.a ); // 2console.log( bar.a ); // 4
new 绑定比隐式绑定的优先级更高
,但 new 绑定和显式绑定谁的优先级更高呢?function foo(something) { this.a = something;}var obj1 = {};var bar = foo.bind( obj1 );bar( 2 );console.log( obj1.a ); // 2var baz = new bar(3);console.log( obj1.a ); // 2console.log( baz.a ); // 3
function foo(p1,p2) { this.val = p1 + p2;}// 之所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么// 反正使用 new 时 this 会被修改var bar = foo.bind( null, "p1" );var baz = new bar( "p2" );baz.val; // p1p2
排序:显式绑定 > new 绑定 > 隐式绑 定 > 默认绑定
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
function foo() { console.log( this.a );}var a = 2;foo.call( null ); // 2
一种非常常见的做法是使用 apply(..) 来“展开”一个数组,并当作参数传入一个函数。
function foo(a,b) { console.log( "a:" + a + ", b:" + b );}// 把数组“展开”成参数foo.apply( null, [2, 3] ); // a:2, b:3// 使用 bind(..) 进行柯里化var bar = foo.bind( null, 2 );bar( 3 ); // a:2, b:3
在 JavaScript 中创建一个空对象最简单的方法都是 Object.create(null)。Object.create(null) 和 {} 很 像, 但 是 并 不 会 创 建 Object.prototype 这个委托,所以它比 {}“更空”:
function foo(a,b) { console.log( "a:" + a + ", b:" + b );}// 我们的 DMZ 空对象var ø = Object.create( null );// 把数组展开成参数foo.apply( ø, [2, 3] ); // a:2, b:3// 使用 bind(..) 进行柯里化var bar = foo.bind( ø, 2 );bar( 3 ); // a:2, b:3
function foo() { console.log( this.a );}var a = 2;var o = { a: 3, foo: foo };var p = { a: 4 };o.foo(); // 3(p.foo = o.foo)(); // 2
注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则this 会被绑定到全局对象。
if (!Function.prototype.softBind) { Function.prototype.softBind = function(obj) { var fn = this; // 捕获所有 curried 参数 var curried = [].slice.call( arguments, 1 ); var bound = function() { return fn.apply( (!this || this === (window || global)) ? obj : this curried.concat.apply( curried, arguments ) ); }; bound.prototype = Object.create( fn.prototype ); return bound; };}
function foo() { console.log("name: " + this.name);}var obj = { name: "obj" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" };var fooOBJ = foo.softBind( obj );fooOBJ(); // name: objobj2.foo = foo.softBind(obj);obj2.foo(); // name: obj2 <---- 看!!!fooOBJ.call( obj3 ); // name: obj3 <---- 看!setTimeout( obj2.foo, 10 );// name: obj <---- 应用了软绑定
箭头函数
箭头函数不适用 this 的四种标准规则,而是根据外层(函数或全局)的作用域来决定 this
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 !
function foo() { var self = this; // this 快照 setTimeout( function(){ console.log( self.a ); }, 100 );}var obj = { a: 2};foo.call( obj ); // 2
Q:(question)
R:(result)
A:(attention matters)
D:(detail info)
S:(summary)
Ana:(analysis)
T:(tips)