MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的this关键字与箭头函数

2024-11-223.7k 阅读

JavaScript 中的 this 关键字基础

this 关键字的概述

在 JavaScript 中,this 是一个特殊的关键字,它的值在函数调用时动态绑定,指向与函数执行上下文相关的对象。它的指向并非取决于函数的声明位置,而是取决于函数的调用方式。这与许多其他编程语言中 this 或类似概念(如 Java 中的 this)有很大不同,在那些语言中 this 通常指向类的实例对象。

全局上下文中的 this

在全局作用域中(即不在任何函数内部),this 指向全局对象。在浏览器环境中,全局对象是 window;在 Node.js 环境中,全局对象是 global。例如:

console.log(this === window); // 在浏览器中输出 true
console.log(this); // 在浏览器中输出 window 对象
// 在 Node.js 环境中
console.log(this === global); // 输出 true
console.log(this); // 输出 global 对象

函数调用中的 this

  1. 普通函数调用
    • 当函数作为普通函数被调用时,this 指向全局对象(在严格模式下,thisundefined)。例如:
function test() {
    console.log(this);
}
test(); // 在非严格模式下,输出 window 对象(浏览器环境)
  • 在严格模式下:
function test() {
    'use strict';
    console.log(this);
}
test(); // 输出 undefined
  1. 作为对象方法调用
    • 当函数作为对象的方法被调用时,this 指向该对象。例如:
const obj = {
    name: 'John',
    sayHello: function() {
        console.log('Hello, I am'+ this.name);
    }
};
obj.sayHello(); // 输出 Hello, I am John
  • 这里 sayHello 函数作为 obj 对象的方法被调用,所以 this 指向 obj,从而可以正确访问 objname 属性。
  1. 使用 call、apply 和 bind 方法改变 this 指向
    • call 方法call 方法允许显式地设置函数内部 this 的值,并立即调用该函数。它的第一个参数是要设置的 this 值,后面可以跟多个参数作为函数的参数。例如:
function greet(greeting) {
    console.log(greeting + ', I am'+ this.name);
}
const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };
greet.call(person1, 'Hello'); // 输出 Hello, I am Alice
greet.call(person2, 'Hi'); // 输出 Hi, I am Bob
  • apply 方法apply 方法与 call 方法类似,也是用于改变函数内部 this 的值并立即调用函数。但它的第二个参数是一个数组,数组中的元素作为函数的参数。例如:
function sum(a, b) {
    return a + b;
}
const numbers = [3, 5];
const result = sum.apply(null, numbers);
console.log(result); // 输出 8
  • bind 方法bind 方法创建一个新的函数,这个新函数内部的 this 被绑定到指定的值。它不会立即调用函数,而是返回一个新的函数。例如:
function sayName() {
    console.log('My name is'+ this.name);
}
const person = { name: 'Charlie' };
const boundFunction = sayName.bind(person);
boundFunction(); // 输出 My name is Charlie

深入理解 this 关键字的绑定规则

默认绑定

  1. 规则描述
    • 当函数被作为普通函数调用,且不在严格模式下时,this 会默认绑定到全局对象。这是因为在这种调用方式下,没有明确的调用上下文对象来指定 this 的值。例如:
function printThis() {
    console.log(this);
}
printThis(); // 在浏览器环境非严格模式下,输出 window 对象
  1. 严格模式下的变化
    • 在严格模式下,默认绑定规则发生变化,this 会被绑定为 undefined。例如:
function printThis() {
    'use strict';
    console.log(this);
}
printThis(); // 输出 undefined

隐式绑定

  1. 对象方法调用场景
    • 当函数作为对象的方法被调用时,this 会隐式绑定到该对象。这是因为函数的调用是通过对象进行的,JavaScript 引擎会将 this 绑定到调用该方法的对象。例如:
const car = {
    brand: 'Toyota',
    describe: function() {
        console.log('This is a'+ this.brand +'car.');
    }
};
car.describe(); // 输出 This is a Toyota car.
  1. 隐式绑定丢失的情况
    • 当将对象的方法赋值给一个变量,然后通过这个变量调用函数时,隐式绑定会丢失,this 会遵循默认绑定规则(在非严格模式下指向全局对象,严格模式下为 undefined)。例如:
const obj = {
    value: 42,
    getValue: function() {
        return this.value;
    }
};
const func = obj.getValue;
console.log(func()); // 在非严格模式下,输出 undefined(因为 this 指向全局对象,全局对象没有 value 属性)

显式绑定

  1. call 方法的原理
    • call 方法的作用是改变函数内部 this 的指向,并立即执行该函数。它的实现原理是将函数作为传入对象的一个临时方法进行调用。例如:
function greet() {
    console.log('Hello,'+ this.name);
}
const person = { name: 'David' };
greet.call(person); // 输出 Hello, David
  • 实际上,call 方法会将 greet 函数添加到 person 对象上,调用 person.greet(),然后再删除这个临时添加的方法。
  1. apply 方法的原理
    • apply 方法与 call 方法类似,区别在于参数的传递方式。apply 方法的第二个参数是一个数组,数组中的元素作为函数的参数。例如:
function sum(a, b) {
    return a + b;
}
const numbers = [2, 3];
const result = sum.apply(null, numbers);
console.log(result); // 输出 5
  • 这里 apply 方法同样改变了 sum 函数内部 this 的指向(这里为 null,在非严格模式下 this 会指向全局对象,严格模式下 thisnull 所代表的值),并使用数组中的元素作为参数调用了函数。
  1. bind 方法的原理
    • bind 方法创建一个新函数,新函数内部的 this 被永久绑定到 bind 方法的第一个参数。例如:
function multiply(x, y) {
    return this.factor * x * y;
}
const boundMultiply = multiply.bind({ factor: 2 });
const result = boundMultiply(3, 4);
console.log(result); // 输出 24
  • bind 方法返回的新函数在调用时,无论以何种方式调用,其内部的 this 始终指向 bind 方法传入的对象。

new 绑定

  1. 构造函数与 new 关键字
    • 在 JavaScript 中,使用 new 关键字调用函数时,该函数被当作构造函数。new 操作符会创建一个新的空对象,这个对象会被链接到构造函数的原型对象上,并且构造函数内部的 this 会指向这个新创建的对象。例如:
function Person(name) {
    this.name = name;
    this.sayHello = function() {
        console.log('Hello, I am'+ this.name);
    };
}
const person = new Person('Eve');
person.sayHello(); // 输出 Hello, I am Eve
  1. new 绑定的过程细节
    • 首先,创建一个新的空对象。
    • 然后,将这个新对象的 __proto__ 属性指向构造函数的 prototype 对象。
    • 接着,使用这个新对象作为 this 的值来调用构造函数。
    • 最后,如果构造函数没有显式返回一个对象,则返回这个新创建的对象。例如:
function Car(make) {
    this.make = make;
    // 这里没有显式返回对象
}
const myCar = new Car('Ford');
console.log(myCar.make); // 输出 Ford
  • 如果构造函数显式返回一个对象,则 new 表达式的结果就是这个返回的对象。例如:
function House() {
    this.color = 'white';
    return {
        rooms: 4
    };
}
const myHouse = new House();
console.log(myHouse.color); // 输出 undefined,因为返回的对象没有 color 属性
console.log(myHouse.rooms); // 输出 4

JavaScript 中的箭头函数与 this

箭头函数的基本语法

箭头函数是 JavaScript 中一种简洁的函数定义方式。它的语法比传统函数更加紧凑,例如:

// 传统函数
function add(a, b) {
    return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
  • 当箭头函数只有一个参数时,可以省略参数的括号,例如:
const square = x => x * x;
  • 当箭头函数没有参数时,需要使用空括号,例如:
const greet = () => console.log('Hello!');

箭头函数中 this 的特殊性

  1. 不绑定自身的 this
    • 箭头函数不绑定自身的 this,它的 this 继承自外层作用域的 this。这与传统函数有很大的区别,传统函数在调用时会根据调用方式动态绑定 this。例如:
const obj = {
    name: 'John',
    regularFunction: function() {
        return function() {
            return this.name;
        };
    },
    arrowFunction: function() {
        return () => this.name;
    }
};
const regularFunc = obj.regularFunction();
const arrowFunc = obj.arrowFunction();
console.log(regularFunc()); // 在非严格模式下,输出 undefined(因为内部函数作为普通函数调用,this 指向全局对象,全局对象没有 name 属性)
console.log(arrowFunc()); // 输出 John(因为箭头函数继承了外层函数的 this,这里外层函数的 this 指向 obj)
  1. 词法作用域的 this
    • 箭头函数的 this 遵循词法作用域,即它的 this 值在定义时就已经确定,而不是在调用时确定。例如:
function outer() {
    this.value = 42;
    const arrow = () => this.value;
    return arrow();
}
const result = outer();
console.log(result); // 输出 42
  • 在这个例子中,箭头函数 arrowthis 继承自 outer 函数的 this,而 outer 函数作为普通函数调用,在非严格模式下 this 指向全局对象(这里假设在全局作用域中定义 outer),如果在严格模式下,outer 函数内部的 thisundefined,那么箭头函数 arrowthis 也为 undefined

箭头函数与事件处理中的 this

  1. 传统函数在事件处理中的 this
    • 在 DOM 事件处理中,传统函数的 this 指向触发事件的 DOM 元素。例如:
<button id="myButton">Click me</button>
<script>
    const button = document.getElementById('myButton');
    button.addEventListener('click', function() {
        console.log(this === button); // 输出 true
    });
</script>
  1. 箭头函数在事件处理中的 this
    • 如果在事件处理中使用箭头函数,它的 this 不会指向触发事件的 DOM 元素,而是继承自外层作用域的 this。例如:
<button id="myButton">Click me</button>
<script>
    const button = document.getElementById('myButton');
    const obj = {
        handleClick: () => {
            console.log(this === window); // 在浏览器环境中输出 true(因为箭头函数的 this 继承自外层作用域,这里外层作用域是全局作用域,this 指向 window)
        }
    };
    button.addEventListener('click', obj.handleClick);
</script>
  • 所以在事件处理中使用箭头函数时,需要注意 this 的指向可能与预期不同。如果需要访问触发事件的 DOM 元素,可以通过将 event.target 作为参数传递给箭头函数来实现。例如:
<button id="myButton">Click me</button>
<script>
    const button = document.getElementById('myButton');
    button.addEventListener('click', (event) => {
        console.log(event.target === button); // 输出 true
    });
</script>

箭头函数与 setTimeout 和 setInterval

  1. 传统函数在 setTimeout 和 setInterval 中的 this
    • setTimeoutsetInterval 中使用传统函数时,函数内部的 this 在非严格模式下指向全局对象,在严格模式下为 undefined。例如:
function Timer() {
    this.count = 0;
    setTimeout(function() {
        console.log(this === window); // 在非严格模式下输出 true
        this.count++;
        console.log(this.count); // 在非严格模式下输出 NaN,因为全局对象没有 count 属性
    }, 1000);
}
new Timer();
  1. 箭头函数在 setTimeout 和 setInterval 中的 this
    • 使用箭头函数时,它的 this 继承自外层作用域的 this。例如:
function Timer() {
    this.count = 0;
    setTimeout(() => {
        console.log(this === Timer.prototype); // 在 new 调用构造函数的情况下,this 指向 Timer 构造函数的实例
        this.count++;
        console.log(this.count); // 输出 1
    }, 1000);
}
new Timer();
  • 所以在 setTimeoutsetInterval 中使用箭头函数可以更方便地访问外层作用域的 this,避免 this 指向混乱的问题。

this 关键字与箭头函数的实际应用场景

在面向对象编程中的应用

  1. 传统函数与 this 在类中的使用
    • 在 JavaScript 的类(ES6 类是基于原型的面向对象编程的语法糖)中,方法通常是传统函数,this 指向类的实例。例如:
class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(this.name +'makes a sound.');
    }
}
const dog = new Animal('Buddy');
dog.speak(); // 输出 Buddy makes a sound.
  1. 箭头函数在类中的注意事项
    • 虽然可以在类中定义箭头函数,但由于箭头函数不绑定自身的 this,可能会导致意外的行为。例如:
class Counter {
    constructor() {
        this.value = 0;
        this.increment = () => {
            this.value++;
        };
    }
}
const counter = new Counter();
const { increment } = counter;
increment(); // 这里会报错,因为箭头函数的 this 继承自外层作用域,这里外层作用域没有 value 属性
  • 通常在类中,方法使用传统函数来确保 this 指向类的实例。

在回调函数中的应用

  1. 传统函数作为回调函数时的 this
    • 当传统函数作为回调函数传递给其他函数时,this 的指向取决于调用方式。例如,在数组的 forEach 方法中,回调函数的 this 在非严格模式下指向全局对象,在严格模式下为 undefined。例如:
const numbers = [1, 2, 3];
function logNumber() {
    console.log(this === window); // 在非严格模式下输出 true
    console.log(this.number + ':'+ this.value); // 在非严格模式下会报错,因为全局对象没有 number 和 value 属性
}
numbers.forEach(logNumber);
  1. 箭头函数作为回调函数时的 this
    • 使用箭头函数作为回调函数时,它的 this 继承自外层作用域的 this。例如:
const obj = {
    number: 42,
    values: [1, 2, 3],
    printValues: function() {
        this.values.forEach((value) => {
            console.log(this.number + ':'+ value);
        });
    }
};
obj.printValues();
// 输出 42: 1
// 输出 42: 2
// 输出 42: 3
  • 这里箭头函数作为 forEach 的回调函数,其 this 继承自 printValues 函数的 this,从而可以正确访问 objnumber 属性。

在模块开发中的应用

  1. 模块中 this 的使用
    • 在 JavaScript 模块中,this 的指向取决于模块的类型(如 ES6 模块、CommonJS 模块等)。在 ES6 模块中,this 通常是 undefined。例如:
// ES6 模块
console.log(this); // 输出 undefined
  • 在 CommonJS 模块(Node.js 中常用)中,this 指向 exports 对象的父对象(通常是 module.exports 的父对象)。例如:
// CommonJS 模块
console.log(this === exports); // 输出 false
console.log(this === module.exports); // 输出 false
console.log(this === module.exports.__proto__); // 输出 true
  1. 箭头函数在模块中的应用
    • 在模块中使用箭头函数时,其 this 同样遵循词法作用域规则。如果在模块顶层定义箭头函数,它的 this 会继承自全局作用域(在浏览器中为 window,在 Node.js 中为 global),但由于 ES6 模块中 this 通常为 undefined,可能会导致一些意外情况。例如:
// ES6 模块
const func = () => console.log(this);
func(); // 输出 undefined
  • 所以在模块开发中,需要注意箭头函数的 this 指向,特别是在涉及到需要访问模块内部特定对象的场景下。

避免 this 关键字与箭头函数使用中的常见错误

混淆箭头函数与传统函数的 this 绑定

  1. 错误示例
    • 常见的错误是在期望 this 动态绑定的场景下使用箭头函数。例如:
const obj = {
    name: 'Tom',
    getSelf: function() {
        return () => this;
    }
};
const result = obj.getSelf()();
console.log(result === obj); // 输出 true,但如果这里期望 result 是一个新的对象,就会出错,因为箭头函数没有自己的 this
  • 这里如果期望 getSelf 返回的函数能够创建一个新的 this 指向不同对象的函数,使用箭头函数就会导致错误,因为箭头函数的 this 继承自外层作用域。
  1. 正确做法
    • 在这种情况下,应该使用传统函数来实现动态的 this 绑定。例如:
const obj = {
    name: 'Tom',
    getSelf: function() {
        return function() {
            return this;
        };
    }
};
const func = obj.getSelf();
const newObj = { name: 'Jerry' };
const result = func.call(newObj);
console.log(result === newObj); // 输出 true

在事件处理中错误使用箭头函数的 this

  1. 错误示例
    • 在 DOM 事件处理中,错误地使用箭头函数可能导致无法正确访问触发事件的元素。例如:
<button id="myButton">Click me</button>
<script>
    const button = document.getElementById('myButton');
    button.addEventListener('click', () => {
        console.log(this === window); // 输出 true,而不是期望的指向 button
        this.style.color ='red'; // 这里会报错,因为 window 没有 style 属性
    });
</script>
  1. 正确做法
    • 可以使用传统函数来确保 this 指向触发事件的元素,或者在箭头函数中通过 event.target 来访问触发事件的元素。例如:
<button id="myButton">Click me</button>
<script>
    const button = document.getElementById('myButton');
    button.addEventListener('click', function() {
        this.style.color ='red';
    });
    // 或者使用箭头函数结合 event.target
    button.addEventListener('click', (event) => {
        event.target.style.color = 'blue';
    });
</script>

在类方法中错误使用箭头函数

  1. 错误示例
    • 在类的方法中错误使用箭头函数可能导致无法访问类的实例属性。例如:
class MyClass {
    constructor() {
        this.value = 10;
        this.printValue = () => console.log(this.value);
    }
}
const instance = new MyClass();
const { printValue } = instance;
printValue(); // 这里会输出 undefined,因为箭头函数的 this 没有指向类的实例
  1. 正确做法
    • 使用传统函数作为类的方法,这样 this 会正确指向类的实例。例如:
class MyClass {
    constructor() {
        this.value = 10;
        this.printValue = function() {
            console.log(this.value);
        };
    }
}
const instance = new MyClass();
const { printValue } = instance;
printValue(); // 输出 10

不理解箭头函数 this 的词法作用域

  1. 错误示例
    • 当在多层嵌套函数中使用箭头函数时,不理解其 this 的词法作用域可能导致错误。例如:
function outer() {
    this.value = 42;
    function inner() {
        const arrow = () => console.log(this.value);
        arrow();
    }
    inner();
}
outer(); // 输出 42,但如果不理解词法作用域,可能会期望不同的结果
  • 这里箭头函数 arrowthis 继承自 outer 函数的 this,如果错误地认为箭头函数有自己独立的 this 绑定,就会对结果感到困惑。
  1. 正确理解
    • 要明确箭头函数的 this 遵循词法作用域,它会从外层作用域继承 this。在这种多层嵌套的情况下,需要清楚每一层函数的 this 指向,以及箭头函数如何继承 this。例如,如果在 inner 函数中改为传统函数并调用 inner 函数时改变 this 的指向,箭头函数的 this 也会相应改变。例如:
function outer() {
    this.value = 42;
    function inner() {
        const arrow = () => console.log(this.value);
        return arrow;
    }
    const innerFunc = inner();
    const newObj = { value: 100 };
    innerFunc.call(newObj); // 输出 100,因为通过 call 改变了 inner 函数的 this 指向,箭头函数继承了这个新的 this
}
outer();