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

JavaScript中的原型方法与this的应用

2022-01-094.7k 阅读

JavaScript 中的原型方法

原型的基本概念

在 JavaScript 中,每个函数都有一个 prototype 属性,这个属性是一个对象,它包含了可以由特定类型的所有实例共享的属性和方法。当我们通过构造函数创建一个新的对象实例时,这个实例会有一个内部属性 [[Prototype]] (在现代 JavaScript 中可以通过 __proto__ 来访问,尽管 __proto__ 是非标准的,但被广泛支持),它指向构造函数的 prototype 对象。

例如,我们定义一个简单的构造函数 Person

function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};
const john = new Person('John');
john.sayHello(); 

在上述代码中,Person.prototype 上定义的 sayHello 方法,所有通过 Person 构造函数创建的实例(如 john)都可以访问到。这是因为实例的 [[Prototype]] 指向了 Person.prototype

原型链

原型链是 JavaScript 实现继承的一种机制。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null)。

继续上面的例子,如果我们在 john 实例上没有找到 sayHello 方法,JavaScript 会沿着 john.__proto__(也就是 Person.prototype)去查找,因为 sayHello 方法定义在 Person.prototype 上,所以可以找到并执行。

假设我们有一个更复杂的继承结构:

function Animal(name) {
    this.name = name;
}
Animal.prototype.move = function() {
    console.log(`${this.name} is moving`);
};
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
    console.log(`${this.name} is barking`);
};
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.move(); 
myDog.bark(); 

这里 Dog 构造函数继承自 Animal 构造函数。Dog.prototype 通过 Object.create(Animal.prototype) 创建,这样 Dog 的实例就可以访问 Animal.prototype 上的方法,如 move。同时 Dog.prototype 又有自己独有的方法 bark

原型方法的优势

  1. 节省内存:因为原型方法是共享的,所有实例都可以访问相同的方法,而不是每个实例都有自己的一份拷贝。例如在上面 Person 的例子中,如果每个 Person 实例都有自己的 sayHello 方法,会浪费大量的内存。
  2. 便于维护和扩展:如果我们需要修改 PersonsayHello 方法,只需要在 Person.prototype 上修改一次,所有实例的行为都会随之改变。
Person.prototype.sayHello = function() {
    console.log(`Hi, my name is ${this.name}`);
};
john.sayHello(); 

这里我们修改了 Person.prototype 上的 sayHello 方法,john 实例的 sayHello 方法行为也发生了改变。

原型方法的局限性

  1. 共享状态问题:虽然共享方法节省内存,但如果在原型上定义了可变的共享数据,可能会导致意外的行为。例如:
function Car() {}
Car.prototype.passengers = [];
const car1 = new Car();
const car2 = new Car();
car1.passengers.push('Alice');
console.log(car2.passengers); 

这里 car1car2 共享 Car.prototype.passengers,所以当 car1passengers 数组中添加元素时,car2passengers 数组也会受到影响。 2. 原型链查找性能:原型链查找需要时间,如果原型链很长,访问属性或方法的性能会下降。因为每次查找都需要沿着原型链向上查找,直到找到目标或者到达顶端。

this 在 JavaScript 中的应用

this 的绑定规则

  1. 全局环境中的 this:在全局作用域中(非严格模式下),this 指向全局对象,在浏览器中是 window,在 Node.js 中是 global
console.log(this === window); 
function logThis() {
    console.log(this === window); 
}
logThis(); 

在严格模式下,全局作用域中的 thisundefined

'use strict';
console.log(this); 
  1. 函数调用中的 this:在普通函数调用中,this 的值取决于函数的调用方式。如果是直接调用,非严格模式下 this 指向全局对象,严格模式下 thisundefined
function normalCall() {
    console.log(this); 
}
normalCall(); 
  1. 方法调用中的 this:当函数作为对象的方法被调用时,this 指向调用该方法的对象。
const obj = {
    message: 'Hello',
    printMessage: function() {
        console.log(this.message); 
    }
};
obj.printMessage(); 
  1. 构造函数调用中的 this:在构造函数内部,this 指向新创建的对象实例。
function Person(name) {
    this.name = name;
    console.log(this); 
}
const john = new Person('John');
  1. 箭头函数中的 this:箭头函数没有自己的 this 绑定,它的 this 取决于外层作用域的 this
const outerThis = this;
const arrowFunction = () => {
    console.log(this === outerThis); 
};
arrowFunction(); 

使用 call、apply 和 bind 改变 this 指向

  1. call 方法call 方法允许我们调用一个函数,并指定 this 的值。它的第一个参数是要绑定的 this 值,后面可以跟多个参数。
function greet(greeting, language) {
    console.log(`${greeting}, I'm ${this.name} and I speak ${language}`);
}
const person = {
    name: 'Jane'
};
greet.call(person, 'Hello', 'English'); 
  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); 
  1. bind 方法bind 方法创建一个新的函数,新函数的 this 被绑定到指定的值。它不会立即调用函数,而是返回一个新的函数。
function multiply(a, b) {
    return this.factor * a * b;
}
const calculator = {
    factor: 2
};
const boundMultiply = multiply.bind(calculator);
const product = boundMultiply(3, 4); 
console.log(product); 

this 在事件处理中的应用

在事件处理函数中,this 的指向取决于事件的绑定方式。

  1. 传统方式绑定事件
<button id="btn" onclick="handleClick()">Click me</button>
<script>
function handleClick() {
    console.log(this.id); 
}
</script>

这里 this 指向触发事件的 DOM 元素,即按钮元素。

  1. 使用 addEventListener 绑定事件
<button id="newBtn">Click me</button>
<script>
const newBtn = document.getElementById('newBtn');
function handleNewClick() {
    console.log(this.id); 
}
newBtn.addEventListener('click', handleNewClick);
</script>

同样,this 指向触发事件的 DOM 元素。但如果使用箭头函数作为事件处理函数:

<button id="arrowBtn">Click me</button>
<script>
const arrowBtn = document.getElementById('arrowBtn');
arrowBtn.addEventListener('click', () => {
    console.log(this); 
});
</script>

这里 this 指向全局对象(在浏览器中是 window),因为箭头函数没有自己的 this,它取的是外层作用域的 this

this 与原型方法的结合

当原型方法被调用时,this 指向调用该方法的对象实例。回到之前 Person 的例子:

function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};
const mary = new Person('Mary');
mary.sayHello(); 

sayHello 方法内部,this 指向 mary 实例,所以可以正确输出 maryname 属性。

如果在原型方法中使用箭头函数,情况会有所不同:

function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = () => {
    console.log(`Hello, I'm ${this.name}`);
};
const bob = new Person('Bob');
bob.sayHello(); 

这里会输出 Hello, I'm undefined,因为箭头函数的 this 取决于外层作用域,而不是调用 sayHello 的实例 bob。在这个例子中,外层作用域的 this 可能是全局对象,而全局对象没有 name 属性,所以是 undefined

避免 this 绑定问题的最佳实践

  1. 使用箭头函数时注意 this 捕获:在需要访问外部作用域 this 的地方使用箭头函数,但要注意它不会改变 this 绑定的特性。
  2. 明确使用 call、apply 或 bind:在需要改变 this 指向时,明确使用这些方法,以避免意外的 this 绑定。
  3. 使用 const 声明变量:使用 const 声明变量可以避免意外地重新赋值,特别是在涉及到 this 相关的逻辑时,有助于保持代码的一致性。
  4. 遵循一致的编码风格:团队内部遵循一致的编码风格,例如在事件处理函数中统一使用普通函数或箭头函数,以减少因 this 绑定不一致导致的错误。

通过深入理解 JavaScript 中的原型方法和 this 的应用,开发者可以编写出更健壮、高效且易于维护的代码。无论是在构建大型应用程序还是简单的脚本,掌握这些核心概念都是至关重要的。在实际开发中,不断练习和总结经验,能够更好地运用它们解决各种编程问题。例如在模块化开发中,合理利用原型方法共享代码,以及正确处理 this 绑定,可以提高模块的复用性和可靠性。同时,在处理复杂的事件驱动逻辑时,对 this 在事件处理函数中的准确把握,能确保程序按照预期运行。总之,原型方法和 this 的应用是 JavaScript 编程的基石,值得开发者深入钻研。