彻底搞懂 JavaScript 的 this:从陷阱到解决方案

引言:为什么要单独讨论JS中的 this

JS 中的 this 之所以需要被单独拿出来讨论,是因为它在行为上与其他例如 Java、C++等 主流编程语言中的 this 有本质区别。

不妨来看个简单对照

下面是一段 C++ 实例对象的代码:

c++ 复制代码
class Person { 
    public: std::string name = "Alice";
    void sayName() { 
        std::cout << this->name << std::endl;
        } 
    }; 
    
int main() {
    Person p;
    std::string name = "Bob";
    p.sayName(); 
    return 0;
}

只要了解过this就必然知道这里最后的运行结果是

bash 复制代码
Alice

但是如果是一个类似的 JavaScript 代码:

js 复制代码
var bar = {
    myName : 'Alice',
    printName: function() {
        console.log(myName);
    } 
}

myName = "Bob"
bar.printName();

最后得到的结果竟然是

bash 复制代码
Bob

而这就是 JS 的 this 的特殊之处了,接下来不妨与我一同重新审视一下这位陌生的老朋友 this 了。

一、 JS 作者挖下的一个陷阱

早期 JavaScript 没有"类"

在 ES6 之前,JavaScript 并没有 class 关键字 ,也没有原生的面向对象语法。

所以开发者只能通过 构造函数 + this + prototype 的方法来实现类似 OOP 的效果,例如:

js 复制代码
// 普通函数当 "构造函数" 使用
function Person(name) {
  // 用 this 给实例绑定属性
  this.name = name;
}

// 再使用 prototype 来共享方法
Person.prototype.sayHi = function() {
  console.log("你好,我是" + this.name);
};

const alice = new Person("Alice");
alice.sayHi();

这里 this 与其他语言 this 用法并没有差别

但问题在于,当我们将对象中的方法传到其他变量中时

js 复制代码
const foo = alice.sayHi; // 合法的
foo();

最后运行的结果却是:

我们原本的期望是 alice.sayHialice 的方法,不管怎么传、怎么用,this 都应该指向 alice,但是在这里我们的this 居然"丢了"。

这就和 "this 应该指向所属对象" 的期望发生了冲突。


陷阱:this 的指向是由函数的 调用方式 决定的

例如:

js 复制代码
var bar = {
    myName: "XIXI",
    printName: function() {
        console.log(myName);     
        console.log(bar.myName);  
        console.log(this.myName); 
        console.log(this);       
    }
}

function foo() {
    return bar.printName
}

let myName = 'Bob'
let _printName = foo();

// 访问对象调用
bar.printName();
// 普通函数调用
_printName(); 

在这里两次调用 printName() 函数看似是相同的,但是实际运行结果会产生偏差,不妨单独展示俩次调用的运行结果来对比一下。

  • bar.printName() 访问对象调用
  • _printName() 普通函数调用

不难发现,前两个输出都是 BobXIXI

  • 运行console.log(myName): 没有涉及 this ,这时myName就变成自由变量 ,沿着 作用域链(printName -> 全局作用域)查找到了全局作用域中的 let myName = Bob
  • 运行console.log(bar.myName): 指定了 this 指向 bar,此时myName也就顺理成章的找到了bar中的 myName: "XIXI"

但是后两者差别就很大了

  • 运行console.log(this.myName):通过后续console.log(this)能看到,在访问对象中this是指向bar对象,但是在普通函数调用中this的指向却是window

这里就是 JS 中 this 的漏洞:

  • 调用对象内的函数,this 就指向在对象中
  • let 申明的变量,不会挂载在全局window对象上

不好的地方:

  • var 申明的变量,会挂载在全局window对象上
  • 当函数没有调用对象,就是普通函数,this 就指向全局对象(window)

根据上述的差异,我们再来重新审视一下调用结果

  • 访问对象调用:this -> bar,所以this.myName也就是XIXI
  • 普通函数调用:this -> window,所以window.myName就是Bob,对吗?别忘了let申明是不会挂载在window 上的,所以最后结果应当是undefined

导致的后果:

根据上文我们知道,在两种情况下会有数据会与 window 产生牵连,也就是 var申明变量普通函数调用

如果在代码中大量使用 var 申明,也就难免会产生 var 申明的变量名 会与 普通函数名 冲突

js 复制代码
function bar() {}
var bar = 123;

这个时候调用 bar就会导致后面的bar = 123将前面的bar() {}覆盖了。

所以多次使用var会导致很多冲突且难管理(全局变量的污染),将window污染了

解决陷阱?

1. 使用严格模式('use strict')

javascript 复制代码
'use strict';
function foo() {
  this.x = 1;
}

2. 使用箭头函数(无自己的 this

kotlin 复制代码
class Counter {
  constructor() {
    this.count = 0;
  }
  // 箭头函数继承外层 this
  handleClick = () => {
    this.count++;
  }
}

二、背景知识 ------ JS 的执行模型与作用域机制

代码执行的三个阶段

编译阶段 -> 创建执行上下文 -> 执行阶段

阶段 主要作用 this 的关系
编译阶段 词法分析、作用域收集、变量/函数提升 this 不参与
创建执行上下文 构建变量环境和词法环境 this 未绑定
执行阶段 逐行执行语句、函数调用、表达式求值 this 根据调用方式动态绑定

词法作用域 与 动态 this 对比

  • 自由变量查找:沿着作用域链向上查找(编译时确定)

  • this 查找:取决于函数如何被调用(运行时决定)

对比示例:
js 复制代码
const obj = {
  name: 'XIXI',
  myName() {
    console.log(this.name); // this 动态绑定
    function foo() {
      console.log(name); // name 是自由变量,走词法作用域
    }
  }
};

三、this 的五种绑定规则(由强到弱)

1. 显式绑定:call / apply / bind

优先级最高,由开发者主动指定 this

js 复制代码
let bar = {
  myName: "极客邦",
  test1: 1
}

function foo() {
  this.myName = "极客时间";
}
// .call() 或 .apply() 或 .bind() 将 this 指定为 bar
foo.call(bar)
foo.apply(bar)
foo.bind(bar)

2. new 绑定

使用 new 构造函数时,this 指向新创建的实例对象

js 复制代码
function CreateObj() {
  // new CreateObj()内部使用步骤:
  // 1. var temObj = {}
  // 2. CreateObj.call(temObj) 将 this 强制指向 temObj
  // 3. temObj.__proto__ = CreateObj.prototype  将新对象的原型链,连接到构造函数的 prototype 上
  // 4. return temObj  返还创建的新对象
  console.log(this)
  this.name = "极客时间"
}
// 如果这里没有使用 new,那么 this 就会默认指向全局定义域中的 window
var myObj = new CreateObj();

3. 隐式绑定(对象方法调用)

函数作为对象属性被调用 → this 指向该对象

js 复制代码
const obj = {
    name: "XIXI",
    sayName: function() {
        console.log(this.name);  // this === obj
    }
};

obj.sayName();  // this 指向 obj
  • 陷阱 :方法赋值给变量后丢失绑定

    js 复制代码
    const obj = { 
     name: 'XIXI', 
     say() { 
         console.log(this.name); 
     } 
    };
    obj.say(); // XIXI
    const f = obj.say;
    f(); // undefined(严格模式下为 TypeError)

4. 默认绑定(普通函数调用)

  • 非严格模式this 指向全局对象(浏览器中为 window

  • 严格模式('use strict')thisundefined

5. 箭头函数

箭头函数不绑定 this ,而是继承外层词法作用域的 this

js 复制代码
const obj = {
  name: 'Bob',
  foo() {
    setTimeout(() => {
      console.log(this.name); // "Bob"
    }, 100);
  }
};

绑定优先级总结
new > call/apply/bind > 对象方法 > 普通函数

相关推荐
我命由我123451 小时前
微信小程序 - scroll-view 的一些要点(scroll-view 需要设置滚动方向、scroll-view 需要设置高度)
开发语言·前端·javascript·微信小程序·小程序·前端框架·js
1024肥宅1 小时前
手写 Promise:深入理解 JavaScript 异步编程的核心
前端·javascript·promise
北杳同学2 小时前
前端一些用得上的有意思网站
前端·javascript·vue.js·学习
Dragon Wu3 小时前
ReactNative Expo 使用总结(基础)
javascript·react native·react.js
真上帝的左手3 小时前
24. 前端-js框架-Electron
前端·javascript·electron
克喵的水银蛇3 小时前
Flutter 弹性布局实战:Row/Column/Flex 核心用法与优化技巧
前端·javascript·typescript
verse_armour3 小时前
东南大学云课堂导出PPT
javascript
涔溪4 小时前
深入了解 Node.js 性能诊断工具 Clinic.js 的底层工作原理
开发语言·javascript·node.js
Neptune14 小时前
js防抖技术:从原理到实践,如何解决高频事件导致的性能难题
前端·javascript