重新认识 JavaScript 中的 this

简介: 重新认识 JavaScript 中的 this

image.png


关于 this 的错误认识

this 指向函数自身

存在这个误解可能是因为在 JavaScript 中既然函数是一个对象,那么就可以在这个函数对象上通过 key/value 的形式存储某些值,比如下面这个例子:

function addCount() {
  this.count++;
  console.log('count result:', this.count)
  console.log('addCount.count:', addCount.count)
}
addCount.count = 0;
for (let i = 0; i < 3; i++) {
  console.log(`=============== ${i+1} ===============`)
  addCount(i)
}
// 以下是输出结果:
=============== 1 ===============
count result: NaN
addCount.count: 0
=============== 2 ===============
count result: NaN
addCount.count: 0
=============== 3 ===============
count result: NaN
addCount.count: 0
复制代码

当执行 addCount.count = 0 时,确实向函数对象 addCount 添加了一个 count 属性,然而函数内部代码 this.count 中的 this 并不是指向 addCount 函数对象自身,这一点可以通过输出结果来验证。

this 指向 函数的作用域

关于作用域的问题,在某种情况下它是正确的,但是在其他情况下它却是错误的,因此不能说 this 就是指向函数的作用域,需要明确的是,this 在任何情况下都不指向函数的 词法作用域.

下面是个比较典型的例子:

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log( this.a );
}
foo(); // ReferenceError: a is not defined
复制代码

这段代码要实现的内容:

首先,函数 foo 中想要通过 this.bar 调用 bar 函数;

  • 正常的做法是直接通过 bar() 调用,而不是使用 this.bar()

其次,函数 bar 中想要通过 this.a 访问函数 foo 中的变量 a;

  • 不能通过 this 来引用一个词法作用域内部的东西

this 是什么

当一个函数被调用时,会创建一个活动记录(也称为执行上下文),这个记录会包含函数调用的位置(调用栈)、函数的调用方法、传入的参数等信息,而 this 就是记录的其中一个属性,会在函数执行的过程中用到.

image.png

简单总结

  • this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件
  • this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

运行时 this 的指向

函数调用的位置

要想确定 " 函数被调用的位置 ",那就得先理解 调用栈调用位置 的关系,可以从调用栈中分析出真正的调用位置的,因为它决定了 this 的绑定,举个例子:

function baz() {
    // 当前调用栈:baz
    // 当前调用位置: 全局作用域
    console.log( "baz" );
    bar(); // <-- bar 的调用位置
}
function bar() {
    // 当前调用栈: baz -> bar
    // 当前调用位置: 在 baz 中
    console.log( "bar" );
    foo(); // <-- foo 的调用位置
}
function foo() {
    // 当前调用栈: baz -> bar -> foo
    // 当前调用位置: 在 bar 中
    console.log( "foo" );
}
baz(); // <-- baz 的调用位置
复制代码

this 绑定规则

如果想要确定 this 的绑定对象,首先得确定函数的调用位置,然后判断需要符合下面四条规则的哪一条即可.

默认绑定

函数被独立调用 时,this 指向 全局对象undefined,当无法应用其他规则时可将此规则作为默认规则.

正常情况下:this —> 全局对象

function foo() {
    console.log( this.a );
}
var a = 2;
foo(); // 2
复制代码

严格模式下:this —> undefined

function foo() {
    "use strict";
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
复制代码

注意:this 的绑定规则完全取决于 调用位置,但要根据模式而定

  • 只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象
  • 严格模式下foo()调用位置 无关.
function foo() {
    console.log( this.a );
}
var a = 2;
(function(){
    "use strict";
    foo(); // 2
})();
复制代码

隐式绑定

调用位置是否有 上下文对象 ,或者说是否被某个对象拥有或者包含.

即当函数引用有 上下文对象 时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象.

下面的例子中,foo 中的 this 就被隐式绑定到 obj 上:

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2
复制代码

对象属性引用链中只有 最顶层最后一层 会影响调用位置,例如:

function foo() {
  console.log(this.a);
}
var obj2 = {
  a: 2,
  foo: foo
};
var obj1 = {
  a: 1,
  obj2: obj2
};
obj1.obj2.foo(); // 2
复制代码

隐式绑定丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,即此时会应用 默认绑定,从而把 this 绑定到 全局对象undefined 上,取决于是否是严格模式.

函数引用传递

function foo() {
  console.log(this.a);
}
var obj = {
  a: 'a in obj',
  foo: foo
};
var bar = obj.foo; // 将函数引用赋值给 bar 变量
var a = "a in global";
bar(); // "a in global"
复制代码

虽然 barobj.foo 的一个引用,但是实际上它引用的是 foo 函数本身,因此 bar() 等价于 foo(),而不等于 obj.foo() 的调用.

JS 内置函数的回调

把定义的函数作为参数传入 JS 内置函数(如 setTimeout)或自己声明的函数,本质上其实也相当于函数引用的传递,即便函数最终调用位置不同,但是都是相当于直接调用函数本身,即应用默认绑定规则.

function foo() {
  console.log(this.a);
}
var obj = {
  a: 'a in obj',
  foo: foo
};
var a = "a in global";
setTimeout(obj.foo, 0);// a in global
复制代码

显式绑定

如果需要显式绑定 this ,可以使用函数原型上的 bind()call()apply() 三个方法来实现.

image.png

如下的例子,分别通过三种显式绑定的方式进行演示:

function foo() {
  console.log(this.a);
}
var obj1 = {
  a: 1
};
var obj2 = {
  a: 2
};
var obj3 = {
  a: 3
};
foo.bind(obj1)();// 1
foo.call(obj2);// 2
foo.apply(obj3);// 3
复制代码

注意:

bind、call、apply 这三个方法,第一个参数是需要传入需要绑定的对象,如果传入的是一个原始值,那么这些原始值会默认被 " 装箱 ",即被转化为(new String(..)new Boolean(..)new Number(..)).

new 绑定

JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数,实际上并不存在所谓的 "构造函数",只有对于函数的 "构造调用".

使用 new 来调用函数(或 函数的构造调用),会自动执行下面的操作:

  1. 创建一个 全新的对象
  2. 新对象会被执行 [[ 原型 ]] 连接
  3. 新对象会绑定到函数调用的 this
  4. 若函数没有返回其他对象,则 new 表达式中的函数调用会自动返回这个新对象
function foo(a) {
  this.a = a;
}
var bar = new foo(1);
console.log(bar.a); // 1
复制代码

this 绑定规则的优先级

上面介绍了 this4 种绑定规则,如果一个调用位置符合多种规则,那么只能通过优先级来区分它们,优先级顺序如下:

  • new 绑定>显式绑定>隐式绑定>默认绑定

下面这个例子中,证明了 显式绑定>隐式绑定>默认绑定 的优先级.

function foo() {
  console.log(this.a);
}
var a = 1;
var obj1 = {
  a: 2,
  foo: foo
};
var obj2 = {
  a: 3,
  foo: foo
};
// 默认绑定
foo(); // 1
// 隐式绑定
obj1.foo(); // 2 
obj2.foo(); // 3 
// 显式绑定
obj1.foo.call( obj2 ); // 3 
obj2.foo.call( obj1 ); // 2
复制代码

下面这个例子,证明了 new 绑定>显式绑定 的优先级.

function foo(something) {
  this.a = something;
}
var obj1 = {
  a: 1,
  foo: foo
};
var obj2 = {
  a: 2
};
// 隐式绑定
obj1.foo(2); // obj1.a = 2
console.log(obj1.a); // 2 
// 显式绑定 
obj1.foo.call( obj2, 3 ); // obj2.a =3
console.log(obj2.a); // 3
// new 绑定
var bar = new obj1.foo(4); // bar.a = 4
console.log(obj1.a); // 2 
console.log(bar.a); // 4
复制代码

this 绑定的例外

this 绑定被忽略

当使用显示绑定(bind、call、apply)进行 this 绑定时,如果给第一个参数传递了 nullundefined,那么它们在调用时会被忽略,实际应用的是 默认绑定规则.

function foo() {
  console.log(this.a);
}
var a = 1;
foo.call(null); // 1
复制代码

函数被间接引用

值得注意的是,有时可能有意无意的会创建一个函数的 "间接引用",在这种情况下,调用这个函数时会应用 默认绑定规则,具体绑定值还要看是否处于 严格模式.

function foo() {
  console.log(this.a);
}
var a = 0;
var obj1 = {
  a: 1,
  foo: foo
};
var obj2 = {
  a: 2
};
obj1.foo(); // 1
(obj2.foo = obj1.foo)(); // 0
复制代码

赋值表达式 obj2.foo = obj1.foo 的返回值是 目标函数的引用,因此调用位置是 foo() ,而不是 obj2.foo() 或者 obj1.foo().

ES6 中的箭头函数

箭头函数并不是使用 function 关键字定义的,而是使用被称为 "胖箭头" (=>)的操作符进行定义的.

  • 它不使用 this4 种绑定规则,而是根据外层(函数或全局)作用域来决定 this 的指向.
  • 它不能通过 new 操作,因为箭头函数没有自己的 this.
  • 它没有普通函数的 arguments 实参列表.
function foo() { // 返回一个箭头函数
  return (a) => { // this 继承自 foo() 
    console.log(this.a);
  };
}
var obj1 = {
  a: 1
};
var obj2 = {
  a: 2
};
var bar = foo.call(obj1);
bar.call(obj2); // 1



目录
相关文章
|
1月前
|
JavaScript 前端开发
javascript中的this
javascript中的this
|
3月前
|
JavaScript 前端开发
错综复杂的this:理清你的JavaScript代码中的指向问题
错综复杂的this:理清你的JavaScript代码中的指向问题
|
1月前
|
JavaScript
JS中改变this指向的六种方法
JS中改变this指向的六种方法
|
6月前
|
JavaScript 前端开发
详解js中的this指向
详解js中的this指向
44 0
|
6月前
|
前端开发 JavaScript
前端基础 - JavaScript之this关键字
前端基础 - JavaScript之this关键字
25 0
|
4天前
|
JavaScript 前端开发
js开发:请解释this关键字在JavaScript中的用法。
【4月更文挑战第23天】JavaScript的this关键字根据执行环境指向不同对象:全局中指向全局对象(如window),普通函数中默认指向全局对象,作为方法调用时指向调用对象;构造函数中指向新实例,箭头函数继承所在上下文的this。可通过call、apply、bind方法显式改变this指向。
7 1
|
1月前
|
JavaScript
JS中call()、apply()、bind()改变this指向的原理
JS中call()、apply()、bind()改变this指向的原理
|
2月前
|
JavaScript 前端开发
JavaScript中this的指向问题
JavaScript中this的指向问题
|
3月前
|
前端开发 JavaScript
揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(下)
揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(下)
|
3月前
|
前端开发 JavaScript
揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(上)
揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(上)