JavaScript | 原型与原型链

前言:JavaScript中原型和原型链是一个非常重要的知识点,对于后续JavaScript中继承以及ES6中引入的类相关的知识点有着非常重要的作用,同时笔者也想通过这篇文章整理下这块的知识点方便大家查阅。

从新建对象说起

要了解这方面的知识点,首先要从JavaScript中新建对象的方式说起,常见的创建对象的方式有下面几种:

  • 对象字面量

    1
    
    let a = {name: 'test'};
    
  • new关键字

    1
    
    let a = new Object();
    
  • 工厂模式

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    function createPerson(name, age, job){
        var o = new Object();
        o.name = name;
        o.age = age;
        o.job = job;
        o.sayName = function(){
            console.log(this.name);
        };
        return o;
    }
    
    var person1 = createPerson('Walter White', 50, 'Chemist');
    var person2 = createPerson('Jesse Pinkman', 20, 'Student');
    
    console.log(person1);
    //{ name: 'zzx', age: 20, job: 'Programmer', sayName: [Function] }
    
  • 构造函数

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.sayName = function () {
            console.log(this.name);
        };
    }
    let person1 = new Person("Walter White", 50, "Chemist");
    let person2 = new Person("Jesse Pinkman", 20, "Student");
    person1.sayName(); // Nicholas
    person2.sayName(); // Greg 
    console.log(person1.sayName == person2.sayName); // false
    

    ❕关于new的原理可以参考笔者写的这篇文章: JavaScript | new原理

    但是使用构造函数创建的对象有个非常不好的地方,就是每当新建对象的时候构造函数内的方法都会重新执行,造成内存的浪费(如下图):

    截图

    所以就有了下面的这种方式。

  • 原型模式与构造函数模式组合创建对象

    • 原型模式
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      function Person() {}
      Person.prototype.name = "Walter White";
      Person.prototype.age = 50;
      Person.prototype.job = "Chemist";
      Person.prototype.sayName = function() {
       console.log(this.name);
      };
      let person1 = new Person();
      person1.sayName(); // "Walter White"
      let person2 = new Person();
      person2.sayName(); // "Walter White"
      console.log(person1.sayName == person2.sayName); // true 
      

    在JavaScript中每个函数都会有一个prototype属性,这个属性指向一个对象(此对象一般称为:原型对象),所以可以将共用的属性与方法设置到prototype(原型对象)上,在原型对象对象上属性与方法是所有实例共享的。

    所以就可以通过将原型模式与构造函数组合使用的方式解决构造函数创建创建对象时造成的内存浪费的问题,具体做法如下:

    • 私有属性定义在构造函数里面。
    • 公共的方法定义在原型对象(prototype)上(防止内存空间的浪费)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
}
  
Person.prototype.sayName = function (){
  console.log('Say my name!');
  console.log(this.name + '!');
}
  
let person1 = new Person("Walter White", 50, "Chemist"); 
let person2 = new Person("Jesse Pinkman", 23, "Student"); 
  
person1.sayName(); // Walter White!
person2.sayName(); // Jesse Pinkman!

显式原型与隐式原型

  • 显示原型(prototype):每个对象(除了null)都具有的属性,该属性指向该对象的原型
  • 隐式原型(__proto__):只有函数(实例)对象才有的属性,该属性指向该函数的原型对象。

在JavaScript中每次调用构造函数创建一个新的实例,都会在该实例的内部创建一个__proto__属性(有些浏览器中叫([[prototype]])),此属性指向构造函数的 prototype 原型对象 上,具体可以参考下面的代码和示例图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
}

Person.prototype.sayName = function (){
  console.log('Say my name!');
  console.log(this.name + '!');
}

let person1 = new Person("Walter White", 50, "Chemist"); 
let person2 = new Person("Jesse Pinkman", 23, "Student"); 

person1.sayName(); // Walter White!
person2.sayName(); // Jesse Pinkman!

console.log(Person.prototype === person1.__proto__); // true

截图

constructor构造函数属性

正常情况下prototype(原型对象)会自动获得一个constructor属性,constructor的意思是构造函数,该属性指回构造函数本身

截图

constructor属性的作用:

  • 用于在原型对象中记录对象引用自哪一个构造函数。

  • 在有些情况下可以将原型对象中的constructor重新指向回原本的构造函数。

    比如刚才提到的 原型模式与构造函数模式组合 的情况:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    function Person(name, age, job){
      this.name = name;
      this.age = age;
      this.job = job;
    }
    
    Person.prototype.sayName = function (){
      console.log('Say my name!');
      console.log(this.name + '!');
    }
    

    如果现在要在prototype上添加非常多方法,就可以采用另一种写法:重写prototype(原型对象)。此时就需要手动的利用constructor属性将 prototype(原型对象)中的constructor重新指向回原本的构造函数。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    function Person(name, age, job){
      this.name = name;
      this.age = age;
      this.job = job;
    }
    // 重写prototype
    Person.prototype = {
      // 重新添加constructor重新回原来的构造函数
      constructor: Person,
      sayName: function(){
        console.log('Say my name!');
        console.log(this.name + '!');
      },
      sayAge: function(){
        console.log(this.name + " is " + this.age + " years old!");
      },
      sayJob: function(){
        console.log(this.name + " is a " + this.job);
      }
    }
    

原型链

  • prototype(原型对象)是一个对象

  • 在新建对象的时候会在该对象的内部创建一个__proto__属性,__proto__属性指向该对象的原型对象。

  • Person.prototype原型对象 作为一个对象,也有__proto__属性,此属性指向的是 Object.prototype。(PS:可以理解成原型的原型

    默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype。这也是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。——《JavaScript高级程序设计》(第四版)

    个人理解:所有的对象都是Object构造函数的实例

  • Object.prototype原型对象 作为一个对象,也有__proto__属性,此时 __proto__ 指向为 null(原型的终点)。

  • 原型连成一条链,称为原型链,为查找对象的成员提供了一条链,通过这条链一次进行查找

截图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name);
    };
}
// 使用new创建对象
let person1 = new Person("Walter White", 50, "Chemist");
console.log(person1); // Person {name: 'Walter White', age: 50, job: 'Chemist', sayName: ƒ}
// Person.prototype原型对象里面的proto属性指向的是 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); //true
// Object.prototype原型对象里面的proto原型 指向为 null
console.log(Object.prototype.__proto__); //null

JS对象成员查找机制

JS中查找对象是根据原型链的机制去查找的,具体规则如下:

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 若没有则查找它的原型(__proto__指向的prototype原型对象)。
  3. 如果还没有则就查找原型对象的原型(Object的原型对象Object.prototype)。
  4. 依此类推直到找到Object__proto__(null)为止。
  5. __proto__对象原型的意义就是为对象成员查找机制提供一个方向(可以理解为原型链中的 )。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name);
    };
}
// 使用new创建
let person1 = new Person("Walter White", 50, "Chemist");
// 使用手写函数创建
person1.othername = 'Heisenberg1';
Person.prototype.othername = 'Heisenberg2';
Object.prototype.othername = 'Heisenberg3';
console.log(person1);
// 更具情况的不同会显示不同的值
console.log(person1.othername);
// toString 方法是在Object上的
console.log(person1.toString());

原型对象this指向

原则:

  1. 只有调用函数的时候才能确认this指向谁。
  2. 一般情况下this指向的是函数的调用者。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var that1;
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    that1 = this;
    console.log(that1);
}

var that2;

Person.prototype.sayName = function () {
    console.log('Say my name!');
    console.log(this.name + '!');
    that2 = this;
}

// 使用new创建
let person1 = new Person("Walter White", 50, "Chemist");

// 1. 在构造函数中,里面this指向的时对象实例 person1
console.log(person1);

// 2. 原型对象方法里面 this 指向的是 实例对象 person1
person1.sayName();
console.log(that2 === person1); // true

结论:

  1. 在构造函数中,里面this指向的时对象实例 person1
  2. 原型对象方法里面 this 指向的是 实例对象 person1

利用原型对象扩展内置对象方法

比如给Stringprototype上添加一个startWith方法:

1
2
3
4
5
String.prototype.startsWith = function (text) {
  return this.indexOf(text) === 0;
};
let msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true

参考文献