JavaScript内核与高级编程之:`JavaScript`的`Prototype`链:从`__proto__`和`prototype`看对象的继承。

各位观众老爷们,晚上好!我是你们的老朋友,今天咱们聊聊JavaScript里让人又爱又恨的Prototype链。这玩意儿就像个家族族谱,搞明白了就能看清对象间的血缘关系,用起来也就能更加得心应手。准备好,咱们这就开始寻根溯源!

开场白:JavaScript的世界,万物皆对象

在JavaScript里,几乎所有东西都是对象。你说“123”是个数字?它也是Number对象。你说“Hello”是个字符串?它也是String对象。就连函数,那也是Function对象。既然都是对象,那就得有个“爹”,也就是从哪里继承来的。这个“爹”的概念,就是Prototype链的核心。

第一幕:__proto__,对象的私生子

首先,咱们来认识一位低调的大佬:__proto__。这玩意儿,读作“dunder proto”,或者叫“双下划线proto”。它像个秘密通道,连接着一个对象和它的原型对象。

  • __proto__的本质: 任何对象(除了 null)都有 __proto__ 属性。这个属性指向的是创建该对象的构造函数的原型对象(prototype)。简单来说,就是告诉我们这个对象“遗传”自哪个“家族”。

  • 代码实战:

    let myObject = {}; // 创建一个空对象
    
    console.log(myObject.__proto__); // 输出: [object Object]  (指向Object.prototype)
    console.log(myObject.__proto__ === Object.prototype); // 输出: true

    这段代码告诉我们,即使是一个空对象,也有 __proto__ 属性,并且它指向 Object.prototype。这意味着,myObject 继承了 Object.prototype 上的所有属性和方法。

  • 重要提示: __proto__ 是一个非标准属性,虽然主流浏览器都支持,但官方更推荐使用 Object.getPrototypeOf()Object.setPrototypeOf() 来获取和设置对象的原型。但是__proto__对于理解原型链的原理至关重要。

第二幕:prototype,构造函数的正牌夫人

接下来,咱们认识另一位举足轻重的人物:prototype。 这位可不是随便的对象都有的,她是构造函数的专属。

  • prototype的本质: prototype 是一个属性,它属于构造函数。 它的值是一个对象,这个对象包含了所有由此构造函数创建的实例所共享的属性和方法。 换句话说,prototype 是实例对象的“公共基因库”。

  • 代码实战:

    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.sayHello = function() {
      console.log("Hello, my name is " + this.name);
    };
    
    let person1 = new Person("Alice");
    let person2 = new Person("Bob");
    
    person1.sayHello(); // 输出: Hello, my name is Alice
    person2.sayHello(); // 输出: Hello, my name is Bob
    
    console.log(person1.__proto__ === Person.prototype); // 输出: true

    在这个例子中,Person.prototype 定义了一个 sayHello 方法。所有通过 new Person() 创建的实例(比如 person1person2)都共享这个方法。 注意 person1.__proto__ 指向了 Person.prototype,这是连接实例和构造函数原型的关键。

  • 深入理解: 构造函数通过 prototype 属性来定义实例对象可以继承的属性和方法。 实例对象通过 __proto__ 属性来访问其构造函数的 prototype 对象。

第三幕:Prototype链,血脉相连的家族

现在,把 __proto__prototype 这两个概念串起来,就能理解Prototype链了。

  • Prototype链的本质: 当你试图访问一个对象的属性时,JavaScript引擎会首先在该对象自身查找。 如果找不到,它会沿着该对象的 __proto__ 属性指向的原型对象上查找。 如果还是找不到,就继续沿着原型对象的 __proto__ 向上查找,直到找到目标属性或者到达原型链的顶端(null)。 这种沿着 __proto__ 向上查找的链条,就是Prototype链。

  • 图解Prototype链:

    假设我们有以下代码:

    function Animal(name) {
      this.name = name;
    }
    
    Animal.prototype.sayName = function() {
      console.log("My name is " + this.name);
    };
    
    function Dog(name, breed) {
      Animal.call(this, name); // 调用父构造函数
      this.breed = breed;
    }
    
    // 设置Dog的原型为Animal的实例
    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.constructor = Dog; // 修正constructor指向
    
    Dog.prototype.bark = function() {
      console.log("Woof!");
    };
    
    let myDog = new Dog("Buddy", "Golden Retriever");
    
    myDog.bark(); // 输出: Woof!
    myDog.sayName(); // 输出: My name is Buddy

    那么,myDog 的Prototype链是这样的:

    1. myDog 对象本身: 拥有 namebreed 属性。
    2. myDog.__proto__ 指向 Dog.prototype: 拥有 bark 属性和 constructor 属性。
    3. Dog.prototype.__proto__ 指向 Animal.prototype: 拥有 sayName 属性和 constructor 属性。
    4. Animal.prototype.__proto__ 指向 Object.prototype: 拥有 toString, valueOf 等通用方法。
    5. Object.prototype.__proto__ 指向 null: 原型链的终点。

    当我们调用 myDog.bark() 时,JavaScript引擎首先在 myDog 对象自身查找 bark 属性,找不到。 然后在 myDog.__proto__ (也就是 Dog.prototype) 上找到 bark 方法,于是执行。

    当我们调用 myDog.sayName() 时,JavaScript引擎首先在 myDog 对象自身查找 sayName 属性,找不到。 然后在 myDog.__proto__ (也就是 Dog.prototype) 上查找 sayName 方法,找不到。 接着在 Dog.prototype.__proto__ (也就是 Animal.prototype) 上找到 sayName 方法,于是执行。

  • Object.create() 的妙用: Object.create(Animal.prototype) 创建了一个新对象,并将该对象的 __proto__ 设置为 Animal.prototype。 这是一种安全地继承原型属性的方法,避免直接修改 Animal.prototype 带来的副作用。

  • constructor 属性的修正: 由于我们使用了 Object.create() 来设置 Dog.prototype,导致 Dog.prototype.constructor 指向了 Animal,而不是 Dog。 因此,我们需要手动修正 Dog.prototype.constructor = Dog;,以确保 constructor 属性的正确指向。

第四幕:原型继承的各种姿势

掌握了Prototype链的原理,就能灵活运用各种原型继承的方式了。

  • 原型链继承: 直接将子构造函数的 prototype 设置为父构造函数的实例。

    function Parent() {
      this.property = 'parent property';
    }
    
    Parent.prototype.getParentProperty = function() {
      return this.property;
    };
    
    function Child() {
      this.childProperty = 'child property';
    }
    
    Child.prototype = new Parent(); // 关键:将Child的原型设置为Parent的实例
    Child.prototype.constructor = Child; // 修正constructor指向
    
    Child.prototype.getChildProperty = function() {
      return this.childProperty;
    };
    
    let child = new Child();
    console.log(child.getParentProperty()); // 输出: parent property
    console.log(child.getChildProperty()); // 输出: child property

    优点: 简单易懂。

    缺点:

    1. 子类型的所有实例都会共享父类型实例的属性。 如果父类型实例的属性是引用类型,那么一个子类型实例修改了该属性,会影响到所有子类型实例。
    2. 创建子类型实例时,无法向父类型构造函数传递参数。
  • 借用构造函数继承(也叫伪造对象或经典继承): 在子构造函数内部调用父构造函数,使用 callapply 方法。

    function Parent(name) {
      this.name = name;
    }
    
    function Child(name, age) {
      Parent.call(this, name); // 关键:借用Parent构造函数
      this.age = age;
    }
    
    let child = new Child('Alice', 10);
    console.log(child.name); // 输出: Alice
    console.log(child.age); // 输出: 10

    优点: 可以在子类型构造函数中向父类型构造函数传递参数。

    缺点:

    1. 只能继承父类型的实例属性和方法,不能继承父类型原型上的属性和方法。
    2. 每个子类型实例都有一份父类型实例属性的副本,造成内存浪费。
  • 组合继承: 结合原型链继承和借用构造函数继承的优点。

    function Parent(name) {
      this.name = name;
      this.colors = ['red', 'blue', 'green'];
    }
    
    Parent.prototype.sayName = function() {
      console.log(this.name);
    };
    
    function Child(name, age) {
      Parent.call(this, name); // 借用构造函数继承属性
      this.age = age;
    }
    
    Child.prototype = new Parent(); // 原型链继承方法
    Child.prototype.constructor = Child; // 修正constructor指向
    
    Child.prototype.sayAge = function() {
      console.log(this.age);
    };
    
    let child1 = new Child('Alice', 10);
    child1.colors.push('black');
    console.log(child1.colors); // 输出: ['red', 'blue', 'green', 'black']
    child1.sayName(); // 输出: Alice
    child1.sayAge(); // 输出: 10
    
    let child2 = new Child('Bob', 12);
    console.log(child2.colors); // 输出: ['red', 'blue', 'green']
    child2.sayName(); // 输出: Bob
    child2.sayAge(); // 输出: 12

    优点: 既可以继承父类型的实例属性和方法,又可以继承父类型原型上的属性和方法。

    缺点: 父类型构造函数会被调用两次,造成一定的性能浪费。

  • 寄生式组合继承: 对组合继承的优化,避免了父类型构造函数被调用两次。

    function Parent(name) {
      this.name = name;
      this.colors = ['red', 'blue', 'green'];
    }
    
    Parent.prototype.sayName = function() {
      console.log(this.name);
    };
    
    function Child(name, age) {
      Parent.call(this, name);
      this.age = age;
    }
    
    // 关键:使用寄生式继承来避免调用Parent构造函数
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    
    Child.prototype.sayAge = function() {
      console.log(this.age);
    };
    
    let child1 = new Child('Alice', 10);
    child1.colors.push('black');
    console.log(child1.colors);
    child1.sayName();
    child1.sayAge();
    
    let child2 = new Child('Bob', 12);
    console.log(child2.colors);
    child2.sayName();
    child2.sayAge();

    优点: 避免了父类型构造函数被调用两次,性能更好。

    缺点: 代码稍微复杂一些。

  • ES6的class继承: ES6 引入了 class 关键字,提供了更简洁的语法糖来实现继承。

    class Parent {
      constructor(name) {
        this.name = name;
      }
    
      sayName() {
        console.log(this.name);
      }
    }
    
    class Child extends Parent {
      constructor(name, age) {
        super(name); // 调用父类的constructor
        this.age = age;
      }
    
      sayAge() {
        console.log(this.age);
      }
    }
    
    let child = new Child('Alice', 10);
    child.sayName(); // 输出: Alice
    child.sayAge(); // 输出: 10

    优点: 语法更简洁,更易于理解。

    缺点: 本质上仍然是基于原型链的继承,只是语法糖而已。

第五幕:总结与升华

Prototype链是JavaScript中实现继承的关键机制。 理解 __proto__prototype 的关系,以及Prototype链的查找过程,能够帮助我们更好地理解JavaScript的面向对象编程。 虽然继承的方式有很多种,但寄生式组合继承和ES6的 class 继承是目前比较推荐的方式。

表格总结各种继承方式:

继承方式 优点 缺点
原型链继承 简单易懂 1. 子类型实例共享父类型实例的属性;2. 无法向父类型构造函数传递参数
借用构造函数继承 可以在子类型构造函数中向父类型构造函数传递参数 1. 无法继承父类型原型上的属性和方法;2. 每个子类型实例都有一份父类型实例属性的副本
组合继承 既可以继承父类型的实例属性和方法,又可以继承父类型原型上的属性和方法 父类型构造函数会被调用两次
寄生式组合继承 避免了父类型构造函数被调用两次,性能更好 代码稍微复杂一些
ES6的class继承 语法更简洁,更易于理解 本质上仍然是基于原型链的继承

收尾:实战演练,融会贯通

好了,今天的讲座就到这里。希望大家通过今天的学习,能够对JavaScript的Prototype链有更深入的理解。 记住,理论学习只是第一步,更重要的是在实际项目中运用这些知识。 只有在实践中不断探索,才能真正掌握JavaScript的精髓。

下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注