深入分析 JavaScript This 绑定的四种规则 (Default, Implicit, Explicit, New),以及 Arrow Functions 对 This 绑定的特殊处理。

JavaScript "This" 大冒险:从小白到老司机

大家好,我是你们今天的导游,带大家一起探索 JavaScript 中最让人头疼,也最让人着迷的 "this" 指针。准备好了吗?系好安全带,我们出发!

很多新手刚开始接触 JavaScript 的时候,都会被 this 搞得晕头转向,觉得它像一个捉摸不透的幽灵,一会儿指向这里,一会儿指向那里。 别担心,今天我们要做的就是把这个幽灵彻底驯服,让它乖乖听话。

我们要学习 this 的四种绑定规则,以及箭头函数 (Arrow Functions) 对 this 的特殊处理。学完之后,保证你对 this 的理解能上一个台阶,以后再也不会被它坑啦!

1. Default Binding (默认绑定)

我们先从最简单的开始:默认绑定。 当 this 的绑定没有任何其他规则适用时,它就会采用默认绑定。 记住,默认绑定在严格模式和非严格模式下行为不同。

1.1 非严格模式

在非严格模式下,默认绑定的 this 指向全局对象。 在浏览器中,全局对象就是 window;在 Node.js 中,全局对象是 global

function whatIsThis() {
  console.log("This is:", this);
}

whatIsThis(); // 输出:This is: window (在浏览器中) 或 This is: global (在 Node.js 中)

这里,whatIsThis() 函数直接被调用,没有任何前缀,也没有被任何对象调用,因此 this 采用了默认绑定,指向了全局对象。

1.2 严格模式

为了代码的严谨性,JavaScript 引入了严格模式。在严格模式下,默认绑定的 this 指向 undefined

"use strict";

function whatIsThisStrict() {
  console.log("This is:", this);
}

whatIsThisStrict(); // 输出:This is: undefined

可以看到,在严格模式下,this 不再指向全局对象,而是 undefined。 这样做是为了避免一些意外的副作用,让你更清楚地知道 this 的指向。

总结:

模式 默认绑定 this 指向
非严格模式 全局对象 (windowglobal)
严格模式 undefined

2. Implicit Binding (隐式绑定)

隐式绑定是指当函数被一个对象 "拥有" 并调用时,this 指向这个 "拥有" 函数的对象。 简单来说,就是看函数被谁调用。

const myObject = {
  name: "小明",
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

myObject.sayHello(); // 输出:Hello, my name is 小明

在这个例子中,sayHello() 函数被 myObject 对象调用,所以 this 指向了 myObject。 这就是隐式绑定。

2.1 链式调用

如果出现链式调用,this 指向的是链条上最后一个调用函数的对象。

const obj1 = {
  name: "obj1",
  obj2: {
    name: "obj2",
    sayName: function() {
      console.log("My name is:", this.name);
    }
  }
};

obj1.obj2.sayName(); // 输出:My name is: obj2

这里,sayName() 函数是被 obj1.obj2 调用的,所以 this 指向 obj2

2.2 隐式丢失

隐式绑定有一个陷阱,叫做 "隐式丢失"。 当你把一个对象的函数赋值给另一个变量,或者把函数作为参数传递给另一个函数时,this 可能会丢失隐式绑定,退回到默认绑定。

const myObject = {
  name: "小明",
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

const sayHelloAlias = myObject.sayHello;
sayHelloAlias(); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)

在这个例子中,我们将 myObject.sayHello 赋值给 sayHelloAlias。 当我们调用 sayHelloAlias() 时,实际上是直接调用了函数,没有任何对象 "拥有" 它,所以 this 退回到了默认绑定。

另一个例子:

function doSomething(callback) {
  callback();
}

const myObject = {
  name: "小明",
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

doSomething(myObject.sayHello); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)

这里,我们将 myObject.sayHello 作为参数传递给 doSomething() 函数。 在 doSomething() 函数内部调用 callback() 时,this 同样丢失了隐式绑定,退回到了默认绑定。

总结:

规则 说明
隐式绑定 函数被对象调用,this 指向该对象
链式调用 this 指向链条上最后一个调用函数的对象
隐式丢失 函数赋值给变量或作为参数传递,可能丢失隐式绑定,退回默认绑定

3. Explicit Binding (显式绑定)

显式绑定允许你明确地指定函数执行时的 this 值。 JavaScript 提供了三个方法来实现显式绑定:call()apply()bind()

3.1 call()

call() 方法允许你调用一个函数,并指定函数内部的 this 值,以及传递给函数的参数。

function sayHello(greeting) {
  console.log(`${greeting}, my name is ${this.name}`);
}

const myObject = {
  name: "小明"
};

sayHello.call(myObject, "你好"); // 输出:你好, my name is 小明

这里,我们使用 call() 方法调用 sayHello() 函数,并将 myObject 作为 this 值传递给它。 这样,sayHello() 函数内部的 this 就指向了 myObject

3.2 apply()

apply() 方法和 call() 方法类似,也允许你调用一个函数,并指定函数内部的 this 值。 唯一的区别是,apply() 方法接受一个数组作为参数,这个数组会被展开成函数的参数。

function sayHello(greeting, age) {
  console.log(`${greeting}, my name is ${this.name}, I am ${age} years old.`);
}

const myObject = {
  name: "小明"
};

sayHello.apply(myObject, ["你好", 18]); // 输出:你好, my name is 小明, I am 18 years old.

这里,我们使用 apply() 方法调用 sayHello() 函数,并将 myObject 作为 this 值传递给它,同时将 ["你好", 18] 数组作为参数传递给函数。

3.3 bind()

bind() 方法会创建一个新的函数,并将指定的 this 值绑定到这个新函数上。 这个新函数不会立即执行,而是返回一个绑定了 this 的函数,你可以稍后执行它。

function sayHello(greeting) {
  console.log(`${greeting}, my name is ${this.name}`);
}

const myObject = {
  name: "小明"
};

const boundSayHello = sayHello.bind(myObject);
boundSayHello("你好"); // 输出:你好, my name is 小明

这里,我们使用 bind() 方法创建了一个新的函数 boundSayHello,并将 myObject 作为 this 值绑定到这个新函数上。 当我们调用 boundSayHello() 时,this 始终指向 myObject

3.4 硬绑定

bind() 创建的绑定是“硬绑定”,意味着即使你尝试使用 call()apply() 改变 this 的值,也无法生效。

function sayHello() {
  console.log("Hello, my name is " + this.name);
}

const person = { name: "Alice" };
const boundSayHello = sayHello.bind(person);

boundSayHello(); // 输出: Hello, my name is Alice

const anotherPerson = { name: "Bob" };
boundSayHello.call(anotherPerson); // 输出: Hello, my name is Alice  (注意: 仍然是 Alice)
boundSayHello.apply(anotherPerson); // 输出: Hello, my name is Alice  (注意: 仍然是 Alice)

总结:

方法 作用 参数 返回值
call() 调用函数,并指定 this 值和参数 第一个参数是 this 值,后面的参数是函数的参数 函数的返回值
apply() 调用函数,并指定 this 值和参数 (参数以数组形式传递) 第一个参数是 this 值,第二个参数是一个包含函数参数的数组 函数的返回值
bind() 创建一个绑定了 this 值的新的函数 (不会立即执行) 第一个参数是 this 值,后面的参数是函数的预设参数,这些参数会在调用新函数时自动传入 一个绑定了 this 值的新的函数,需要手动调用才能执行

4. New Binding (New 绑定)

当使用 new 关键字调用一个函数时,会发生以下几件事:

  1. 创建一个新的空对象。
  2. 将这个新对象的 [[Prototype]] 链接到构造函数的 prototype 属性。
  3. 将这个新对象绑定到函数调用的 this
  4. 如果函数没有返回其他对象,则返回这个新对象。

因此,当使用 new 关键字调用函数时,this 指向这个新创建的对象。

function Person(name) {
  this.name = name;
  this.sayHello = function() {
    console.log(`Hello, my name is ${this.name}`);
  };
}

const person1 = new Person("小明");
person1.sayHello(); // 输出:Hello, my name is 小明

const person2 = new Person("小红");
person2.sayHello(); // 输出:Hello, my name is 小红

在这个例子中,我们使用 new 关键字调用 Person() 函数,创建了两个新的对象 person1person2。 在 Person() 函数内部,this 指向了新创建的对象,因此我们可以设置对象的属性和方法。

总结:

规则 说明
New 绑定 使用 new 关键字调用函数时,this 指向新创建的对象

5. 优先级

当多个绑定规则同时适用时,它们的优先级是什么样的呢? 记住这个优先级:

New Binding > Explicit Binding > Implicit Binding > Default Binding

New 绑定优先级最高,Default 绑定优先级最低。

function sayHello() {
  console.log("Hello, my name is " + this.name);
}

const person1 = { name: "Alice" };
const person2 = { name: "Bob" };

const boundSayHello = sayHello.bind(person1); // Explicit Binding

boundSayHello(); // 输出: Hello, my name is Alice

new boundSayHello(); // 输出: Hello, my name is undefined (非严格模式) 或 报错 (严格模式)  因为new boundSayHello()忽略了bind的绑定.

function Person(name) {
    this.name = name;
    this.sayHello = function() {
        console.log("Hello, my name is " + this.name);
    }
}

const p = new Person("Charlie"); // New Binding
p.sayHello.call(person2); // 这里 call 并没有生效,因为 sayHello 中的 this 已经被 p 绑定了。

6. Arrow Functions (箭头函数)

箭头函数是 ES6 引入的一种新的函数语法。 箭头函数最大的特点就是它没有自己的 this。 箭头函数会捕获其所在上下文的 this 值,并永久绑定到这个值。 这被称为 词法作用域this 绑定。

const myObject = {
  name: "小明",
  sayHello: function() {
    setTimeout(() => {
      console.log(`Hello, my name is ${this.name}`);
    }, 1000);
  }
};

myObject.sayHello(); // 输出:Hello, my name is 小明 (1秒后)

在这个例子中,setTimeout() 函数内部的回调函数是一个箭头函数。 箭头函数捕获了 sayHello() 函数的 this 值,也就是 myObject。 因此,箭头函数内部的 this 指向 myObject

如果不用箭头函数,代码会是这样的:

const myObject = {
  name: "小明",
  sayHello: function() {
    const self = this; // 关键:用一个变量保存 this
    setTimeout(function() {
      console.log(`Hello, my name is ${self.name}`);
    }, 1000);
  }
};

myObject.sayHello(); // 输出:Hello, my name is 小明 (1秒后)

可以看到,使用箭头函数可以避免 this 丢失的问题,代码更加简洁易懂。

箭头函数不能使用 call()apply()bind() 方法来改变 this 的值。 因为箭头函数的 this 是词法作用域的,一旦绑定就无法改变。

const myObject = {
  name: "小明"
};

const sayHello = () => {
  console.log(`Hello, my name is ${this.name}`);
};

sayHello.call(myObject); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)
sayHello.apply(myObject); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)

const boundSayHello = sayHello.bind(myObject);
boundSayHello(); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)

箭头函数也不能用作构造函数。 因为箭头函数没有自己的 this,所以不能使用 new 关键字来创建对象。

const Person = (name) => {
  this.name = name; // 报错:  Cannot set properties of undefined (setting 'name')
};

const person = new Person("小明"); // 报错: Person is not a constructor

总结:

特点 说明
没有自己的 this 捕获其所在上下文的 this 值,并永久绑定
无法改变 this 不能使用 call()apply()bind() 方法来改变 this 的值
不能用作构造函数 因为没有自己的 this,不能使用 new 关键字来创建对象

7. 总结

规则 描述 优先级 适用情况
Default Binding 函数独立调用,没有其他规则适用 最低
  • 非严格模式下,this 指向全局对象 (windowglobal)。
  • 严格模式下,this 指向 undefined
Implicit Binding 函数被对象 "拥有" 并调用 较低 函数作为对象的属性被调用,this 指向该对象。
Explicit Binding 使用 call()apply()bind() 显式指定 this 较高 需要明确控制函数执行时的 this 值的情况。例如,将一个对象的函数借给另一个对象使用。
New Binding 使用 new 关键字调用函数 最高 构造函数用于创建对象,this 指向新创建的对象。
Arrow Functions 箭头函数没有自己的 this,捕获其所在上下文的 this 值并永久绑定 N/A
  • 希望 this 值继承自外层作用域的情况。
  • 回调函数,避免 this 丢失。
  • 需要简洁的函数语法。

注意:箭头函数不能用作构造函数,也不能使用 call()apply()bind() 改变 this 的值。

希望通过这次 "This" 大冒险,你已经对 JavaScript 中的 this 指针有了更深入的理解。 记住这些规则,多加练习,你就能彻底驯服这个幽灵,让它为你所用! 祝你编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注