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

JavaScript原型链的深层次理解与应用

2023-01-171.7k 阅读

JavaScript 原型链基础概念

在 JavaScript 中,每个对象都有一个 [[Prototype]] 内部属性(在现代 JavaScript 中可以通过 Object.getPrototypeOf() 方法或 __proto__ 属性来访问,__proto__ 是非标准但被广泛支持)。这个 [[Prototype]] 指向另一个对象,而这个被指向的对象就是原型对象。原型对象本身也可能有自己的 [[Prototype]],以此类推,形成了一条链式结构,这就是原型链。

构造函数与原型对象

当我们使用构造函数创建对象时,构造函数的 prototype 属性会成为新创建对象的原型。例如:

function Person(name) {
    this.name = name;
}
// Person.prototype 就是通过 new Person() 创建的对象的原型
const person1 = new Person('Alice');

在上述代码中,person1 的原型就是 Person.prototype。我们可以通过 Object.getPrototypeOf(person1) === Person.prototype 来验证这一点,结果为 true

原型链的作用

原型链的主要作用是实现继承和属性查找。当我们访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端(null)。例如:

function Animal() {
    this.species = 'animal';
}
function Dog(name) {
    this.name = name;
}
Dog.prototype = new Animal();
const myDog = new Dog('Buddy');
// 查找 species 属性,myDog 本身没有,沿着原型链在 Animal 的实例(也就是 Dog.prototype)上找到
console.log(myDog.species); 

在这个例子中,myDog 本身没有 species 属性,但是通过原型链,JavaScript 引擎在 myDog.__proto__(也就是 Dog.prototype,它是 Animal 的一个实例)上找到了 species 属性。

原型链中的关键属性和方法

constructor 属性

每个原型对象都有一个 constructor 属性,它指向关联的构造函数。例如:

function Car(make) {
    this.make = make;
}
const myCar = new Car('Toyota');
console.log(myCar.constructor === Car); 
// Car.prototype.constructor 指向 Car 构造函数
console.log(Car.prototype.constructor === Car); 

这个 constructor 属性在需要通过原型对象创建新实例时很有用。例如:

function Shape() {
    this.color = 'white';
}
function Circle(radius) {
    this.radius = radius;
}
Circle.prototype = Object.create(Shape.prototype);
// 修正 constructor 属性
Circle.prototype.constructor = Circle;
const myCircle = new Circle.prototype.constructor(5);

在上述代码中,当我们修改了 Circle.prototype 后,需要手动修正 constructor 属性,以便通过 Circle.prototype.constructor 能正确创建新的 Circle 实例。

isPrototypeOf() 方法

isPrototypeOf() 方法用于判断一个对象是否是另一个对象原型链上的原型。例如:

function Parent() {}
function Child() {}
Child.prototype = Object.create(Parent.prototype);
const parentObj = new Parent();
const childObj = new Child();
console.log(Parent.prototype.isPrototypeOf(childObj)); 
console.log(Child.prototype.isPrototypeOf(childObj)); 
console.log(childObj.isPrototypeOf(Parent.prototype)); 

在这段代码中,Parent.prototypechildObj 原型链上的原型,所以 Parent.prototype.isPrototypeOf(childObj) 返回 trueChild.prototype 也是 childObj 的原型,所以 Child.prototype.isPrototypeOf(childObj) 也返回 true;而 childObj 不是 Parent.prototype 的原型,所以 childObj.isPrototypeOf(Parent.prototype) 返回 false

hasOwnProperty() 方法

hasOwnProperty() 方法用于判断一个对象是否拥有某个自身属性(而不是从原型链继承来的属性)。例如:

function Fruit(name) {
    this.name = name;
}
const apple = new Fruit('Apple');
console.log(apple.hasOwnProperty('name')); 
console.log(apple.hasOwnProperty('color')); 

在上述代码中,apple 有自身的 name 属性,所以 apple.hasOwnProperty('name') 返回 trueapple 没有自身的 color 属性,所以 apple.hasOwnProperty('color') 返回 false,即使 color 属性可能从原型链上获取到。

原型链的深入理解

原型链顶端

原型链的顶端是 Object.prototype,几乎所有的 JavaScript 对象最终都会继承自它。例如:

function MyObject() {}
const myObj = new MyObject();
let currentPrototype = Object.getPrototypeOf(myObj);
while (currentPrototype!== null) {
    console.log(currentPrototype);
    currentPrototype = Object.getPrototypeOf(currentPrototype);
}

在这段代码中,通过不断获取原型对象,最终会到达 Object.prototype,然后再向上获取原型就是 null,标志着原型链的结束。Object.prototype 提供了一些通用的方法,如 toString()valueOf() 等,这些方法被所有继承自它的对象所共享。

原型链与函数对象

在 JavaScript 中,函数也是对象,它们同样有原型链。所有函数对象的原型都是 Function.prototype。例如:

function greet() {
    console.log('Hello!');
}
console.log(Object.getPrototypeOf(greet) === Function.prototype); 

这意味着函数对象可以使用 Function.prototype 上定义的方法,比如 call()apply()bind()。这些方法对于控制函数的执行上下文非常重要。例如:

function add(a, b) {
    return a + b;
}
const result1 = add.call(null, 2, 3); 
const result2 = add.apply(null, [2, 3]); 
const boundAdd = add.bind(null, 2);
const result3 = boundAdd(3); 

在上述代码中,call()apply() 方法允许我们在指定的上下文(这里是 null)中调用 add 函数,并传递参数;bind() 方法则创建一个新的函数,将 add 函数的第一个参数固定为 2,返回的新函数 boundAdd 只需要再传递一个参数就可以完成加法运算。

原型链与数组对象

数组对象也有自己的原型链。Array.prototype 为数组提供了丰富的方法,如 push()pop()map() 等。例如:

const numbers = [1, 2, 3];
console.log(Object.getPrototypeOf(numbers) === Array.prototype); 
const newLength = numbers.push(4);

在这段代码中,numbers 是一个数组对象,它的原型是 Array.prototypepush() 方法就是定义在 Array.prototype 上的,通过原型链,numbers 对象可以使用这个方法向数组末尾添加元素。

原型链的应用场景

实现继承

原型链是 JavaScript 实现继承的主要方式。通过设置一个对象的原型为另一个对象,我们可以让前者继承后者的属性和方法。例如经典的动物继承示例:

function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound.`);
};
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} barks.`);
};
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); 
myDog.bark(); 

在这个例子中,Dog 构造函数通过设置 Dog.prototypeAnimal.prototype 的一个实例,实现了对 Animal 的继承。Dog 实例不仅可以使用自身定义的 bark() 方法,还可以使用从 Animal 继承来的 speak() 方法。

代码复用

原型链使得我们可以在多个对象之间共享代码。例如,我们有一个需要在多个对象中使用的方法 logDetails

function Loggable() {}
Loggable.prototype.logDetails = function() {
    for (let prop in this) {
        if (this.hasOwnProperty(prop)) {
            console.log(`${prop}: ${this[prop]}`);
        }
    }
};
function User(name, age) {
    this.name = name;
    this.age = age;
}
User.prototype = Object.create(Loggable.prototype);
User.prototype.constructor = User;
const user1 = new User('Bob', 30);
user1.logDetails(); 

在上述代码中,User 对象通过原型链继承了 Loggable.prototype 上的 logDetails 方法,实现了代码的复用,避免了在 User 构造函数中重复编写相同的代码。

扩展内置对象

我们还可以利用原型链来扩展内置对象。例如,我们想为数组添加一个新的方法 sum 来计算数组元素的总和:

if (!Array.prototype.sum) {
    Array.prototype.sum = function() {
        return this.reduce((acc, num) => acc + num, 0);
    };
}
const numbers = [1, 2, 3, 4];
const sum = numbers.sum(); 

在这段代码中,我们检查 Array.prototype 上是否已经有 sum 方法,如果没有则添加它。这样所有的数组对象都可以通过原型链使用这个新方法。

原型链相关的陷阱与注意事项

意外修改原型

在修改原型对象时要格外小心,因为这会影响到所有基于该原型的对象。例如:

function Shape() {
    this.color = 'white';
}
function Circle(radius) {
    this.radius = radius;
}
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
const circle1 = new Circle(5);
const circle2 = new Circle(10);
// 意外修改原型
Circle.prototype.color = 'red';
console.log(circle1.color); 
console.log(circle2.color); 

在上述代码中,我们意外地修改了 Circle.prototypecolor 属性,这会导致 circle1circle2 以及未来创建的所有 Circle 实例的 color 属性都被改变。

原型链查找性能

原型链查找会带来一定的性能开销,尤其是当原型链很长时。每次查找属性都需要沿着原型链向上遍历,直到找到属性或者到达原型链顶端。例如:

function A() {}
function B() {}
function C() {}
function D() {}
B.prototype = Object.create(A.prototype);
C.prototype = Object.create(B.prototype);
D.prototype = Object.create(C.prototype);
const d = new D();
// 查找一个在 A.prototype 上的属性,需要遍历多层原型链
console.log(d.someProperty); 

在这个例子中,如果 d 查找一个在 A.prototype 上的属性,JavaScript 引擎需要从 D.prototype 开始,依次向上遍历 C.prototypeB.prototype 最后到 A.prototype,这在性能敏感的场景下可能会成为瓶颈。

构造函数与原型的关系混乱

在创建对象和设置原型时,要确保构造函数和原型之间的关系正确。如果错误地设置了原型,可能会导致 constructor 属性不正确,以及继承关系混乱。例如:

function Parent() {}
function Child() {}
// 错误的设置原型方式
Child.prototype = Parent.prototype;
const child = new Child();
console.log(child.constructor === Child); 

在上述代码中,Child.prototype = Parent.prototype 这种设置方式会使 child.constructor 指向 Parent 而不是 Child,正确的方式应该是 Child.prototype = Object.create(Parent.prototype) 并修正 constructor 属性。

原型链与 ES6 类

ES6 类的底层实现与原型链

ES6 引入了 class 关键字,使得 JavaScript 可以使用类似传统面向对象语言的语法来定义类。但实际上,ES6 类只是基于原型链的语法糖。例如:

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}
class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    bark() {
        console.log(`${this.name} barks.`);
    }
}
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); 
myDog.bark(); 

在这段代码中,Dog 类继承自 Animal 类。从原型链的角度来看,Dog.prototypeAnimal.prototype 的一个实例,Dog 实例可以通过原型链访问 Animal 类的方法。我们可以通过以下方式验证:

console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); 

类的静态方法与原型链

ES6 类可以定义静态方法,这些方法直接定义在类本身,而不是原型上。例如:

class MathUtils {
    static add(a, b) {
        return a + b;
    }
}
console.log(MathUtils.add(2, 3)); 

在这个例子中,add 方法是 MathUtils 类的静态方法,它不会出现在 MathUtils.prototype 上,所以不能通过 MathUtils 类的实例来调用。静态方法主要用于提供与类相关但不需要实例化就能使用的功能。而实例方法则定义在原型上,通过原型链供实例使用。例如:

class Counter {
    constructor() {
        this.count = 0;
    }
    increment() {
        this.count++;
    }
    getCount() {
        return this.count;
    }
}
const counter = new Counter();
counter.increment();
console.log(counter.getCount()); 

在这个 Counter 类中,incrementgetCount 方法定义在 Counter.prototype 上,counter 实例可以通过原型链访问并调用这些方法。

原型链在框架与库中的应用

在 jQuery 中的应用

jQuery 是一个广泛使用的 JavaScript 库,它利用原型链来实现对象的方法共享。例如,当我们创建一个 jQuery 对象时:

<!DOCTYPE html>
<html>

<head>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>

<body>
    <div id="myDiv">Hello, World!</div>
    <script>
        const $div = $('#myDiv');
        $div.css('color','red');
    </script>
</body>

</html>

在上述代码中,$div 是一个 jQuery 对象,它的原型链上定义了许多方法,如 css()。这些方法定义在 jQuery.prototype 上,所有的 jQuery 对象都可以通过原型链访问和使用这些方法,实现了代码的复用和简洁性。

在 React 中的应用

虽然 React 主要关注组件化开发,但原型链在其内部机制中也有一定的作用。例如,React 组件是通过类或者函数定义的。当使用类定义组件时,它基于 JavaScript 的原型链机制。例如:

import React, { Component } from'react';
class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            message: 'Initial message'
        };
    }
    render() {
        return <div>{this.state.message}</div>;
    }
}

在这个 MyComponent 类中,它继承自 React.ComponentReact.Component 提供了一些原型方法,如 setState()MyComponent 实例可以通过原型链访问这些方法来更新组件的状态。这种基于原型链的继承方式使得 React 组件可以复用 React.Component 的功能,同时保持自身的特性。

在 Vue.js 中的应用

Vue.js 在其组件系统中也利用了原型链。Vue 组件是通过 Vue.extend() 方法或者 Vue.component() 注册的。例如:

<!DOCTYPE html>
<html>

<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>

<body>
    <div id="app">
        <my-component></my-component>
    </div>
    <script>
        Vue.component('my-component', {
            data() {
                return {
                    text: 'Hello from component'
                };
            },
            template: '<div>{{text}}</div>'
        });
        const app = new Vue({
            el: '#app'
        });
    </script>
</body>

</html>

在这个例子中,my - component 组件的原型链上包含了 Vue 实例的一些方法和属性,使得组件可以使用 Vue 的数据响应式系统、生命周期钩子等功能。这种基于原型链的方式有助于构建可复用、可组合的组件结构。

通过深入理解 JavaScript 原型链的概念、关键属性和方法、应用场景以及相关的陷阱,开发者能够更好地利用原型链来编写高效、可维护的 JavaScript 代码,无论是在原生开发还是在使用各种框架和库的项目中。同时,结合 ES6 类等新特性,原型链的使用变得更加灵活和强大。