JavaScript中this关键字的系统解析

52 阅读4分钟

JavaScript中this关键字的系统解析

一、this的基本概念与设计初衷

JavaScript中的this关键字是函数运行时的上下文对象 ,它的设计初衷是为了让函数能够知道"自己属于谁",解决函数复用时的上下文问题 。在JavaScript中,函数可以被多个对象共享,而this正是实现这种灵活性的关键机制。

this的作用是让函数能够根据不同的调用者访问不同的对象属性和方法。例如,一个显示名称的函数可以被不同对象调用,通过this就可以正确获取各自对象的名称属性:

function sayName() {
    console.log(this.name);
}

const user1 = { name: "Alice", say: sayName };
const user2 = { name: "Bob", say: sayName };

user1.say(); // 输出 "Alice"
user2.say(); // 输出 "Bob"

在早期JavaScript中,由于没有class这样的面向对象语法,this成为实现对象导向编程的核心机制。通过this,开发者可以将函数作为对象的方法,实现对对象内部状态的访问和修改。

二、this的指向规则

JavaScript中的this指向遵循一套明确的规则,这些规则决定了函数执行时this到底指向哪个对象。以下是四种主要的this绑定规则:

1. 默认绑定规则

当函数作为普通函数调用,没有任何上下文对象时,this会应用默认绑定规则:

  • 非严格模式:this指向全局对象(在浏览器中是window对象)
  • 严格模式:this为undefined
function showContext() {
    console.log(this);
}

showContext(); // 非严格模式下输出: Window {...},严格模式下输出 undefined

严格模式的引入是为了避免意外污染全局对象,当函数在严格模式下调用时,如果调用方式没有明确指定上下文对象,this会保持undefined值 。

2. 隐式绑定规则

当函数作为对象的一个属性被调用时,this会隐式地绑定到这个对象上 :

const user = {
    name: 'Alice',
    greet: function() {
        console.log(`Hello, my name is ${this.name}.`);
    }
};

user.greet(); // 输出 "Hello, my name is Alice."

隐式绑定的陷阱:当对象方法被赋值给一个变量后,这个变量调用函数时会失去隐式绑定,转而应用默认绑定 :

const myObj = {
    name: '极客时间',
    showThis: function() {
        console.log(this);
    }
};

// 正常情况,this指向myObj
myObj.showThis(); // 输出 {name: '极客时间', showThis: ƒ}

// 隐式绑定失效的情况
var foo = myObj.showThis; // 将方法赋值给变量
foo(); // 非严格模式下输出 window,严格模式下输出 undefined

这是因为函数调用时,JavaScript引擎会检查函数是否被对象属性调用,如果是,则this指向该对象;否则,应用默认绑定规则。

3. 显式绑定规则

通过Function对象的call、apply或bind方法,可以显式地指定函数执行时的this值 :

function foo() {
    this.name = '极客帮';
    console.log(this);
}

// 使用call方法显式绑定this
foo.call({ test: 1 }); // 输出 {test: 1, name: '极客帮'}

// 使用apply方法显式绑定this
foo.apply({ test: 2 }); // 输出 {test: 2, name: '极客帮'}

// 使用bind方法创建新函数
var boundFoo = foo.bind({ test: 3 });
boundFoo(); // 输出 {test: 3, name: '极客帮'}

call和apply方法会立即执行函数,而bind方法会创建一个新的函数,该函数在调用时会保持指定的this值 。

4. 构造函数绑定规则

当函数被new关键字调用时,this指向新创建的对象实例:

function CreatObj() {
    this.name = '极客时间';
}

var myObj = new CreatObj();
console.log(myObj.name); // 输出 "极客时间"

在构造函数调用时,JavaScript引擎会自动创建一个空对象,将构造函数的this绑定到该对象,并将该对象返回 。

三、this与执行上下文的关系

JavaScript的执行上下文(Execution Context)是函数执行时的运行环境,包含三个核心部分:变量对象(Variable Object)、作用域链(Scope Chain)和this值 。this是执行上下文的一部分,其值由函数的调用方式决定,而非函数的定义方式。

执行上下文通过调用栈(Call Stack)管理,全局上下文首先入栈,处于栈底。当遇到新的函数调用时,对应的执行上下文会被创建并入栈,直到函数执行完毕后出栈 。

严格模式对执行上下文的影响尤为明显:

 function strictShowThis() {
     console.log(this); // 严格模式下输出 undefined
 }
 'use strict';
 strictShowThis(); // 输出 undefined

在严格模式下,全局上下文的this值不再是全局对象,而是undefined,这有助于防止意外修改全局变量,提高代码安全性 。

四、this在实际应用中的表现

1. 普通函数调用场景

当函数直接调用时,this会应用默认绑定规则:

function foo() {
    console.log(this); // window
}

foo(); // 作为普通函数被调用时,this 指向全局对象

这是因为foo函数声明在全局作用域中,相当于window foo()的调用方式 。这种情况下,this的指向可能会污染全局对象,尤其是在非严格模式下 。

2. 对象方法调用场景

当函数作为对象的方法被调用时,this指向调用该方法的对象:

var bar = {
    myName: 'time.geekbang.com',
    printName: function() {
        console.log bar.myName); // 输出 "time.geekbang.com"
        console.log(this); // 输出 window
        console.log(this.myName); // 输出 undefined
    }
};

bar.printName(); // 作为对象方法调用时,this指向全局对象,而非bar对象

这里有一个常见的误解:对象方法中的this并不自动指向该对象。在非严格模式下,this指向全局对象,而在严格模式下会指向undefined。要正确访问对象属性,应使用显式对象引用(如bar.myName)。

3. 显式绑定场景

通过call、apply或bind方法可以显式控制this的指向:

function foo() {
    this.myName = '极客帮';
    console.log(this);
}

var bar = {
    myName: 'time.geekbang.com',
    test1: 1
};

// 使用call方法显式绑定this
foo.call bar); // 输出 {myName: '极客帮', test1: 1}
console.log bar.myName); // 输出 "极客帮"

// 使用apply方法显式绑定this
foo.apply bar); // 输出 {myName: '极客帮', test1: 1}

这在处理需要特定上下文的函数时非常有用,例如将工具函数复用在不同对象上 。

4. 构造函数场景

在构造函数中,this指向新创建的对象实例:

function CreatObj() {
    this.name = '极客时间';
}

var myObj = new CreatObj();
console.log myObj.name); // 输出 "极客时间"

构造函数中的this绑定是JavaScript实现面向对象编程的基础,它允许我们创建具有相同结构但不同状态的对象实例 。

5. 事件处理函数场景

在事件处理函数中,this通常指向触发事件的元素:

document.getElementById('link')
.addEventListener('click', function() {
    console.log(this); // 输出触发点击的 <a> 元素
});

这在DOM编程中非常有用,允许我们直接访问事件目标的属性和方法。但需要注意,在异步回调中,this可能会丢失绑定,需要特别处理 。

五、this与自由变量查找机制的区别

JavaScript中this的指向规则与自由变量查找机制有本质区别:

自由变量查找是在函数定义时确定的,遵循词法作用域(Lexical Scope)规则 。自由变量是指在函数内部使用,但既不是函数的参数,也不是函数的局部变量,而是来自外层作用域的变量。例如:

function outer() {
    let myName = '极客时间';
    function inner() {
        console.log(myName); // 自由变量查找,指向外层作用域的myName
    }
    return inner;
}

const innerFunc = outer();
innerFunc(); // 输出 "极客时间"

this的指向则是在函数调用时确定的,与函数定义的位置无关,而是由调用方式决定 。例如:

const person = {
    name: 'Tom',
    sayHi: function() {
        console.log(this.name); // this指向调用该方法的对象
    }
};

person sayHi(); // 输出 "Tom"

const sayHi = person sayHi; // 方法赋值给变量
sayHi(); // 非严格模式下输出 undefined,严格模式下报错

在严格模式下,sayHi()调用会报错,因为this是undefined,而尝试访问undefined.name会抛出异常 。

六、this的使用技巧与最佳实践

1. 始终使用严格模式

在JavaScript代码中启用严格模式,可以避免this默认绑定到全局对象,防止意外污染全局变量:

 function myFunction() {
     console.log(this); // undefined
 }
 'use strict';
 myFunction(); // 输出 undefined

严格模式下的this绑定规则更加严格,有助于编写更安全的代码 。

2. 合理使用箭头函数

箭头函数没有自己的this值,而是继承自包围它的非箭头函数的词法作用域 :

const obj = {
    name: 'Alice',
    say: () => {
        console.log(this.name); // 输出 undefined
    },
    talk: function() {
        constbaz = () => {
            console.log(this.name); // 输出 "Alice"
        };
        baz();
    }
};

obj say(); // 输出 undefined
obj talk(); // 输出 "Alice"

箭头函数适合在需要固定this指向的场景使用,例如对象方法中的内部函数或异步回调 。

3. 显式绑定this

对于需要特定上下文的函数,可以在定义时使用bind方法显式绑定this:

const obj = {
    name: '极客时间',
    greet: function() {
        console.log(`Hello, I'm ${this.name}.`);
    }
};

// 显式绑定this
const boundGreet = obj.greet.bind(obj);

// 即使赋值给变量,也能正确指向
const greet = boundGreet;
greet(); // 输出 "Hello, I'm 极客时间."

这可以避免隐式绑定失效的问题,提高代码的可预测性和安全性 。

4. 在类方法中使用this

ES6引入的class语法简化了面向对象编程,类中的方法默认使用隐式绑定规则:

class Person {
    constructor(name) {
        this.name = name;
    }

    greet() {
        console.log(`Hello, I'm ${this.name}.`);
    }
}

const tom = new Person('Tom');
tom.greet(); // 输出 "Hello, I'm Tom."

但需要注意,在类方法中使用箭头函数会导致this绑定到词法作用域,而非实例:

class Person {
    constructor(name) {
        this.name = name;
        this greeter = () => {
            console.log(`Hello, I'm ${this.name}.`);
        };
    }

    static greetStatic() {
        console.log(`Hello, I'm ${this.name}.`); // this指向Person类
    }
}

const tom = new Person('Tom');
tom greeter(); // 输出 "Hello, I'm Tom."
Person.greetStatic(); // 输出 "Hello, I'm undefined."

因此,在类中定义方法时,应避免使用箭头函数,除非明确需要词法this绑定。

5. 处理异步回调中的this

在异步回调函数中,this可能会丢失绑定,需要特别处理:

class User {
    constructor(name) {
        this.name = name;
    }

    startTask() {
        setTimeout(function() {
            console.log(`Task completed by ${this.name}.`); // 输出 "Task completed by undefined."
        }, 1000);
    }

    startTaskBound() {
        setTimeout(function() {
            console.log(`Task completed by ${this.name}.`); // 输出 "Task completed by Alice."
        }.bind(this), 1000);
    }

    startTaskArrow() {
        setTimeout(() => {
            console.log(`Task completed by ${this.name}.`); // 输出 "Task completed by Alice."
        }, 1000);
    }
}

const alice = new User('Alice');
alice.startTask(); // 严格模式下报错,非严格模式下输出 "Task completed by undefined."
alice.startTaskBound(); // 输出 "Task completed by Alice."
alice.startTaskArrow(); // 输出 "Task completed by Alice."

在异步回调中,this通常会指向全局对象或undefined,因此需要使用bind方法或箭头函数来保留正确的this指向。

七、this设计的争议与权衡

JavaScript的this设计一直存在争议,主要围绕其"动态绑定"特性:

支持观点认为,this的动态绑定是JavaScript灵活性的体现,允许函数在不同上下文中复用,实现面向对象编程的多种模式。

反对观点则指出,this的动态绑定容易导致意外行为,尤其是在函数被赋值给变量后调用时 。例如:

const obj = {
    name: '极客时间',
    updateName: function(newName) {
        this.name =name;
    }
};

// 正确调用
obj.updateName('极客帮');
console.log(obj.name); // 输出 "极客帮"

// 错误调用
const update = obj.updateName;
update('极客时间'); // 非严格模式下不会修改obj.name,严格模式下报错

这种设计使得this的指向难以预测,增加了代码维护的难度。然而,正是这种灵活性使得JavaScript能够适应多种编程范式,成为如此强大的语言。

八、总结与展望

JavaScript的this机制是语言设计中的一个独特特征,它通过动态绑定实现了函数的灵活性和面向对象的特性。虽然这种设计可能导致一些意外行为,但通过严格模式、箭头函数和显式绑定等现代特性,我们可以在保持灵活性的同时提高代码的安全性和可预测性。

随着JavaScript的发展,ES6引入的class语法和箭头函数为我们提供了更清晰的this绑定机制。未来,可能还会出现更多特性来简化this的使用,但理解其基本原理仍然是每个JavaScript开发者必备的技能。

通过深入理解this的指向规则、与执行上下文的关系以及在实际应用中的表现,我们可以更自信地驾驭JavaScript的this机制,编写出更高效、更安全的代码。