欢迎光临
我们一直在努力

JavaScript原型链 - 继承的基石与核心机制

JavaScript原型链 – 继承的基石与核心机制

  • 前言:从一道面试题说起
  • 构造函数、实例、原型的三者关系
  • prototype和__proto__的区别与联系
  • 完整的原型链结构
  • 属性屏蔽规则
    • hasOwnProperty vs in操作符
    • 属性屏蔽的三种情况
      • 情况1:对象自身有该属性(完全屏蔽)
      • 情况2:对象自身本来没有,但添加对象自身属性
      • 情况3:属性是只读的
  • 原型链的基础结构
  • 原型链的实际应用
    • 实现继承的多种方式
      • 原型链继承(经典方式)
      • 组合继承(最常用)
      • 寄生组合式继承(最佳实践)
      • ES6 class继承
    • 实现混入(Mixin)
  • 原型链常见陷阱
    • 陷阱1:修改原型会影响所有实例
    • 陷阱2:原型上的引用类型属性被所有实例共享
    • 陷阱3:for…in会遍历原型链上的可枚举属性
  • 原型链的最佳实践
    • 实践1:使用Object.create设置原型链
    • 实践2:使用class语法(ES6+)
    • 实践3:安全地检查属性
    • 实践4:避免修改内置对象的原型
  • 思考题:以下代码的输出是什么?为什么?
  • 结语

前言:从一道面试题说起

function Person() {}
const person = new Person();

console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

console.log(person.constructor === Person); // true
console.log(Person.constructor === Function); // true
console.log(person.constructor === Person.prototype.constructor); // true (!)
console.log(Function.constructor === Function); // true (!)

如果你能完全理解上面的代码,那么你已经掌握了原型链的核心。如果不能,本篇文章将带我们一步步揭开原型链的神秘面纱。

构造函数、实例、原型的三者关系

在JavaScript中,每个对象都有一个特殊的内部属性[[Prototype]](可以通过__proto__访问),它指向该对象的原型对象。

function Person(name) {
this.name = name;
}

// 原型对象:所有实例共享的方法和属性
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};

// 实例:通过new创建的对象
const zhangsan = new Person('zhangsna');

// 三者关系验证
console.log(zhangsan.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

prototype和__proto__的区别与联系

  • prototype 是函数特有的属性。
  • __proto__ 是每个对象都有的属性。

function Foo() {}
const obj = new Foo();

console.log(typeof Foo.prototype); // "object"
console.log(typeof obj.__proto__); // "object"

console.log(Foo.prototype === obj.__proto__); // true
console.log(Foo.__proto__ === Function.prototype); // true

// 一定要注意:函数也是对象,所以函数也有__proto__
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true

  • 每个函数都有一个 prototype 属性,指向该函数的原型对象。
  • 每个对象都有一个 __proto__ 属性,指向创建该对象的构造函数的原型对象。
  • 原型对象的 constructor 属性指向创建该实例对象的构造函数。
  • Function 是一个特殊的函数,它的 constructor 指向它自己。
  • 完整的原型链结构

    function Animal(name) {
    this.name = name;
    }

    Animal.prototype.eat = function() {
    console.log(`${this.name} is eating`);
    };

    function Dog(name, breed) {
    Animal.call(this, name); // 调用父类构造函数
    this.breed = breed;
    }

    // 设置原型链
    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.constructor = Dog; // 修复constructor指向

    Dog.prototype.bark = function() {
    console.log(`${this.name} is barking`);
    };

    const myDog = new Dog('Buddy', 'Golden Retriever');

    // 原型链查找路径:
    console.log(myDog.__proto__ === Dog.prototype); // true
    console.log(Dog.prototype.__proto__ === Animal.prototype); // true
    console.log(Animal.prototype.__proto__ === Object.prototype); // true
    console.log(Object.prototype.__proto__); // null

    上述代码中,原型链的查找过程:

  • myDog 本身有 name 和 breed 属性
  • myDog.__proto__ (Dog.prototype) 有 bark 方法
  • Dog.prototype.__proto__ (Animal.prototype) 有 eat 方法
  • Animal.prototype.__proto__ (Object.prototype) 有 toString 等方法
  • Object.prototype.__proto__ 为null,查找结束
  • Object.prototype是所有原型链的终点。

    属性屏蔽规则

    hasOwnProperty vs in操作符

    • hasOwnProperty: 检查属性是否在对象自身(不在原型链上)
    • in操作符: 检查属性是否在对象自身或原型链上

    属性屏蔽的三种情况

    function Parent() {
    this.value = 'parent value';
    }

    Parent.prototype.shared = 'parent shared';

    function Child() {
    this.value = 'child value';
    }

    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;

    Child.prototype.shared = 'child shared';

    const child = new Child();

    情况1:对象自身有该属性(完全屏蔽)

    以上述代码为例,Child 本身有自己的 value 属性,当调用 value 属性时,会完全屏蔽原型链上的同名 value 属性:

    console.log(child.value); // 'child value'(不是'parent value')
    console.log(child.hasOwnProperty('value')); // true

    情况2:对象自身本来没有,但添加对象自身属性

    console.log(child.shared); // 'child shared'(来自Child.prototype)
    console.log(child.hasOwnProperty('shared')); // false

    // 添加对象自身属性
    child.shared = 'instance shared';
    console.log(child.shared); // 'instance shared'(现在自身有了)
    console.log(child.hasOwnProperty('shared')); // true

    情况3:属性是只读的

    Parent.prototype.readOnly = 'cannot change';

    // 试图修改只读属性
    child.readOnly = 'try to change';
    console.log(child.readOnly); // 'cannot change'(修改失败)
    console.log(child.hasOwnProperty('readOnly')); // false(没有添加成功)

    原型链的基础结构

    function Person(name) {
    this.name = name;
    }

    Person.prototype.sayHello = function() {
    console.log(`Hello, ${this.name}`);
    };

    const zhangsan = new Person('zhangsan');

    其原型结构图如下:

    zhangsan (实例)
    ├── __proto__: Person.prototype
    │ ├── constructor: Person
    │ ├── sayHello: function()
    │ └── __proto__: Object.prototype
    │ ├── constructor: Object
    │ ├── toString: function()
    │ ├── hasOwnProperty: function()
    │ └── __proto__: null
    ├── name: "zhangsan"
    └── (其他实例属性…)

    Person (构造函数)
    ├── prototype: Person.prototype
    └── __proto__: Function.prototype
    ├── constructor: Function
    ├── apply: function()
    ├── call: function()
    └── __proto__: Object.prototype

    原型链的实际应用

    实现继承的多种方式

    原型链继承(经典方式)

    function Parent(name) {
    this.name = name;
    }

    function Child(age) {
    this.age = age;
    }

    // 关键:让子类原型指向父类实例
    Child.prototype = new Parent();

    // 修复constructor指向
    Child.prototype.constructor = Child;

    • 问题:引用类型的属性会被所有实例共享

    组合继承(最常用)

    function Parent(name) {
    this.name = name ;
    }

    function Child(name, age) {
    // 继承属性
    Parent.call(this, name); // 第二次调用Parent
    this.age = age;
    }

    // 继承方法
    Child.prototype = new Parent(); // 第一次调用Parent
    Child.prototype.constructor = Child;

    • 优点:结合了原型链和构造函数的优点
    • 缺点:父类构造函数被调用了两次

    寄生组合式继承(最佳实践)

    function inheritPrototype(child, parent) {
    // 创建父类原型的副本
    const prototype = Object.create(parent.prototype);
    // 修复constructor指向
    prototype.constructor = child;
    // 设置子类原型
    child.prototype = prototype;
    }

    function Parent(name) {
    this.name = name;
    }

    function Child(name, age) {
    Parent2.call(this, name);
    this.age = age;
    }

    inheritPrototype(Child, Parent);

    • 只调用一次父类构造函数
    • 避免在子类原型上创建不必要的属性
    • 原型链保持不变

    ES6 class继承

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

    class Child3 extends Parent3 {
    constructor(name, age) {
    super(name);
    this.age = age;
    }

    sayAge() {
    console.log(this.age);
    }
    }

    • 语法简洁,现代解决方案,但需要ES6+支持

    实现混入(Mixin)

    const canEat = {
    eat: function(food) {
    console.log(`${this.name} is eating ${food}`);
    this.energy += 10;
    }
    };

    const canSleep = {
    sleep: function(hours) {
    console.log(`${this.name} is sleeping for ${hours} hours`);
    this.energy += hours * 5;
    }
    };

    const canWalk = {
    walk: function(distance) {
    console.log(`${this.name} is walking ${distance} km`);
    this.energy -= distance * 2;
    }
    };

    // 混入函数
    function mixin(target, sources) {
    Object.assign(target.prototype, sources);
    }

    // 创建动物类
    function Animal(name) {
    this.name = name;
    this.energy = 100;
    }

    mixin(Animal, canEat, canSleep, canWalk);

    // 创建鸟类,额外添加飞行能力
    const canFly = {
    fly: function(distance) {
    console.log(`${this.name} is flying ${distance} km`);
    this.energy -= distance * 5;
    }
    };

    function Bird(name) {
    Animal.call(this, name);
    this.wings = 2;
    }

    // 设置原型链
    Bird.prototype = Object.create(Animal.prototype);
    Bird.prototype.constructor = Bird;

    // 添加飞行能力
    Object.assign(Bird.prototype, canFly);

    原型链常见陷阱

    陷阱1:修改原型会影响所有实例

    function Person(name) {
    this.name = name;
    }

    const zhangsan = new Person('zhangsan');
    const lisi = new Person('lisi');

    // 修改原型
    Person.prototype.sayHello = function() {
    console.log(`Hello, ${this.name}`);
    };

    zhangsan.sayHello(); // Hello, zhangsan (正常)
    lisi.sayHello(); // Hello, lisi (正常)

    陷阱2:原型上的引用类型属性被所有实例共享

    function Problem() {
    this.values = []; // 正确:每个实例有自己的数组
    }

    Problem.prototype.sharedValues = []; // 错误:所有实例共享同一个数组

    const p1 = new Problem();
    const p2 = new Problem();

    p1.sharedValues.push('from p1');
    p2.sharedValues.push('from p2');

    console.log(p1.sharedValues); // ['from p1', 'from p2']
    console.log(p2.sharedValues); // ['from p1', 'from p2']

    陷阱3:for…in会遍历原型链上的可枚举属性

    function Parent() {
    this.parentProp = 'parent';
    }

    Parent.prototype.inheritedProp = 'inherited';

    function Child() {
    this.childProp = 'child';
    }

    Child.prototype = new Parent();

    const child = new Child();

    for (let key in child) {
    console.log(key); // childProp, parentProp, inheritedProp
    }

    解决方案:使用hasOwnProperty过滤:

    for (let key in child) {
    if (child.hasOwnProperty(key)) {
    console.log(key); // childProp, parentProp
    }
    }

    原型链的最佳实践

    实践1:使用Object.create设置原型链

    function Parent(name) {
    this.name = name;
    }

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

    function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
    }

    // 最佳方式
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;

    // 添加子类方法
    Child.prototype.sayAge = function() {
    console.log(this.age);
    };

    实践2:使用class语法(ES6+)

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

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

    class GoodChild extends GoodParent {
    constructor(name, age) {
    super(name);
    this.age = age;
    }

    sayAge() {
    console.log(this.age);
    }
    }

    实践3:安全地检查属性

    const obj = { ownProp: 'value' };

    // 不好的做法
    if (obj.property) {
    // 如果property值为falsy(0, '', false, null, undefined),会被误判
    }

    // 好的做法
    if (obj.hasOwnProperty('property')) {
    // 明确检查自身属性
    }

    // 更好的做法(防止hasOwnProperty被覆盖)
    if (Object.prototype.hasOwnProperty.call(obj, 'property')) {
    // 最安全的方式
    }

    实践4:避免修改内置对象的原型

    // 非必要情况,不得进行以下操作
    if (!Array.prototype.customMethod) {
    Array.prototype.customMethod = function() {
    // 实现
    };
    }

    思考题:以下代码的输出是什么?为什么?

    function Foo() {}
    function Bar() {}

    Bar.prototype = Object.create(Foo.prototype);

    const bar = new Bar();

    console.log(bar instanceof Bar);
    console.log(bar instanceof Foo);
    console.log(bar instanceof Object);

    console.log(Bar.prototype.isPrototypeOf(bar));
    console.log(Foo.prototype.isPrototypeOf(bar));
    console.log(Object.prototype.isPrototypeOf(bar));

    结语

    原型链是 JavaScript 面向对象编程的基石,在 JavaScript 中没有真正的类,只有对象和它们之间的链接(原型链),对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

    赞(0)
    未经允许不得转载:171主机测评 » JavaScript原型链 - 继承的基石与核心机制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址