this 之谜揭底:从浅入深理解 JavaScript 中的 this 关键字(二)(上)

简介: this 之谜揭底:从浅入深理解 JavaScript 中的 this 关键字(二)

this 之谜揭底:从浅入深理解 JavaScript 中的 this 关键字(二)


调用位置


  • • 在理解 this 的绑定过程之前,首先要理解调用位置调用位置就是函数在代码中被调用的位置(而不是声明的位置)
  • • 通常来说,寻找调用位置就是寻找"函数被调用的位置", 最重要的要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。运行代码时,调用器会在那个位置暂停,同时会在展示当前位置的函数调用列表,这就是调用栈。


绑定规则


  • • 函数的调用位置决定了 this 的绑定对象,通常情况下分为以下几种规则:

默认绑定

  • • 最常用的函数调用类型:独立函数调用。可把这条规则看到是无法应用其他规则时的默认规则。
function foo(){
    console.log(this.a);
}
var a = 2;
foo(); // 2
  • • 当调用 foo() 时,this.a 被解析成了全局变量 a。为什么?
  • • 因为在上述代码中,函数调用时应用了this 的默认绑定,因此 this 指向全局对象。(要理解 this,就要先理解调用位置)
  • 如果使用严格模式(strict mode),那全局对象将无法使用默认绑定,因此 this 会绑定到 undefined。
function foo(){
    "use strict";
    console.log(this.a);
}
var a = 2;
foo(); // Type: this is undefined
  • • 虽然 this 的绑定规则完全取决于调用位置,但是只有 foo() 运行在非 strict mode下时,默认绑定才能绑定到全局对象; 严格模式下与 foo() 的调用位置无关。
function foo(){
    console.log(this.a);
}
var a = 2;
(function (){
    "use strict";
    foo(); // 2
})
  • • 通常情况下,尽量减少在代码中混合使用 strict modenon-strict mode,尽量减少在代码中混合使用 strict mode 和 non-strict mode。

隐式绑定

  • 另一条规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或包裹。
  • • 考虑以下代码:
function foo() {
    console.log(this.a); // 2
}
var obj = {
    a: 2,
    foo: foo
}
obj.foo();
  • • 上述代码中,调用位置使用 obj 的上下文来引用函数,可以说函数被调用时 obj 对象拥有或包含它。
  • 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 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
  • • 隐式丢失:在被隐式绑定的函数会丢失绑定对象,也就是说它会默认绑定,从而把 this 绑定到全局对象或 undefined 上,这取决于是否是严格模式。
  • • 如下代码:
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 3
console.log( b ); // 5

new绑定

  • • 在传统的语言中,构造函数时一个特殊方法,使用 new 初始化需要调用的类,通常形式下是 let something = new MyClass();
  • • 在使用 new 来调用函数,会自动执行以下操作:
  1. 1. 创建一个新对象
  2. 2. 让新对象的 __proto__(隐式原型) 等于函数的 prototype(显式原型)
  3. 3. 绑定 this, 让新象绑定于函数的 this 指向
  4. 4. 判断返回值,如果返回值不是一个对象,则返回刚新建的新对象。


优先级


  • • 如果在某个调用位置应用多条规则该如何?那为了解决此问题,那就引申出了优先级问题。
  • • 毫无疑问,默认绑定的优先级是四条规则中最低的,可以先不考虑它。
  • • 先来看看隐式绑定和显式绑定那个优先级更高?
function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
// 隐式绑定
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 = {
    foo: foo
};
var obj2 = {};
// 隐式绑定
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
// new绑定
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
  • • 可以看出,new 绑定比隐式绑定的优先级更高,但 new 绑定和显式绑定谁的优先级更高呢?
  • • new 与 call/apply 无法一起使用,因此无法通过 new foo.call(obj1) 来进行测试,但可以通过硬绑定来测试他两的优先级。
  • • 硬绑定:Function.prototype.bind(...) 会创建一个新的包装函数,这个函数会忽略当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上。
  • • 这样看起来硬绑定(也是显式绑定的一种)似乎比 new 绑定的优先级更高,无法使用 new 来控制 this 绑定。
function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
  • • 出乎意料! bar 被硬绑定到 obj1 上,但是 new bar(3) 并没有像我们预计的那样把 obj1.a 修改为 3。相反, new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。因为使用了 new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。
  • • 硬绑定中的bind(...) 的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数传递给下层的函数(这种技术称为"部分应用",是"柯里化"的一种)。
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
  • 判断this
  1. 1. 是否在 new 中调用(new 绑定), this 指向新创建的对象
  2. 2. 是否通过 call、apply(显示绑定),this 指向绑定的对象
  3. 3. 是否在某个对象中调用(隐式绑定),this 指向绑定的上下文对象
  4. 4. 如果都不是,则是默认绑定,在严格模式下,this 指向 undefined, 非严格模式下,this 指向全局对象。
  • 优先级问题
  • • 显式绑定:call()、apply()。(硬绑定也是显式绑定的其中一种: bind())
  • • new 绑定: new Foo()
  • • 隐式绑定: obj.foo();
  • • 默认绑定: foo();
  • 排序:显式绑定 > new 绑定 > 隐式绑 定 > 默认绑定
相关文章
|
1月前
|
JavaScript 前端开发 安全
ECMAScript 6(以下简称 ES6)的出现为 JavaScript 带来了许多新的特性和改进,其中 let 和 const 是两个非常重要的关键字。
ES6 引入了 `let` 和 `const` 关键字,为 JavaScript 的变量管理带来了革新。`let` 提供了块级作用域和暂存死区特性,避免变量污染,增强代码可读性和安全性;`const` 用于声明不可重新赋值的常量,但允许对象和数组的内部修改。两者在循环、函数内部及复杂项目中广泛应用,有助于实现不可变数据结构,提升代码质量。
26 5
|
1月前
|
前端开发 JavaScript 开发者
除了 async/await 关键字,还有哪些方式可以在 JavaScript 中实现异步编程?
【10月更文挑战第30天】这些异步编程方式在不同的场景和需求下各有优劣,开发者可以根据具体的项目情况选择合适的方式来实现异步编程,以达到高效、可读和易于维护的代码效果。
|
3月前
|
JavaScript 前端开发 Java
JavaScript 保留关键字
JavaScript 保留关键字
21 2
|
3月前
|
JavaScript 前端开发
JavaScript this 关键字
JavaScript this 关键字
17 1
|
4月前
|
JavaScript 前端开发
JavaScript 语句标识符(关键字)
【8月更文挑战第29天】
27 5
|
4月前
|
JavaScript 前端开发
|
6月前
|
JavaScript 前端开发 开发者
JavaScript中的const关键字解析
JavaScript中的const关键字解析
|
6月前
|
JavaScript 前端开发
JavaScript变量命名规则及关键字详解
JavaScript变量命名规则及关键字详解
|
6月前
|
自然语言处理 JavaScript 前端开发
在JavaScript中,this关键字的行为可能会因函数的调用方式而异
【6月更文挑战第15天】JavaScript的`this`根据调用方式变化:非严格模式下直接调用时指向全局对象(浏览器为window),严格模式下为undefined。作为对象方法时,`this`指对象本身。用`new`调用构造函数时,`this`指新实例。`call`,`apply`,`bind`可显式设定`this`值。箭头函数和绑定方法有助于管理复杂场景中的`this`行为。
62 3
|
5月前
|
JavaScript
js 【详解】函数中的 this 指向
js 【详解】函数中的 this 指向
44 0