原型&原型链 | 8月更文挑战

174 阅读9分钟

1. 前言

说到原型,就必然涉及构造函数、类等概念,我会把这部分放到后边的拓展中,如果不清楚的可以先看拓展部分~

本文是找了一些资料(文章)看了后做的总结,有些地方可能理解的不是很深刻,有错误一定要帮我指出来,非常感谢~
最近在看《你不知道的Javascript》,后边看了原型的内容后会再更新==

2. 原型和原型链

正式开始前,大家记住这个:

  • 对象:拥有__proto__constructor属性;
    • __proto__constructor对象独有的。
  • 函数:拥有prototype__proto__constructor属性;
    • prototype函数独有的;
    • 由于JS中函数也是对象,所以函数也有__proto__constructor属性。
  • JS中引用类型都是对象,都有prototype__proto__constructor属性;
  • 注:[[Prototype]]__proto__其实是一回事:都表示原型链中的“连接”。在 JavaScript语言标准中用的[[prototype]] (官方的),而__proto__是很多浏览器提供的 (非官方);

2.1. 原型prototype

  • prototype函数独有的;
  • prototype就是一个对象,又叫做原型对象,可以通过函数.prototype访问;
  • prototype里通常有两个属性:__proto__constructor
  • 原型可以用来共享方法;
  • 原型中this的指向是实例。

2.2. 隐式原型__proto__

  • __proto__对象独有的;
  • 对象通过__proto__访问父构造函数的原型:对象.__proto__ === 父构造函数.prototype

2.3. 构造器constructor

  • constructor对象独有的;
  • 对象的构造函数指向父构造函数原型上的constructor属性:对象.constructor === 父构造函数.prototype.constructor
    • 一般来说,构造函数原型的constructor指向构造函数本身,即:对象.constructor === 父构造函数.prototype.constructor === 父构造函数(特殊情况见constructor可能会丢失);
    • 注意:父构造函数原型里没有constructor属性时,会通过原型链找到再上一级构造函数原型的constructor(详见constructor可能会丢失)。

注意:constructor可能会丢失

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

const p = new Person('p');
p.name; // p
Person.prototype; // {} constructor属性丢失!
// 实例p的constructor不是Person!
p.constructor === Person; // false
// 往Person上边找
p.constructor === Person.prototype.__proto__.constructor // true
Person.prototype.__proto__.constructor === Object // true
p.constructor === Object // true

上例中,Person.prototype赋值为空对象后,Person自身没有constructor属性了。此时通过原型链,Person.constrctor指向了Person.prototype.__proto__.constructorObject,从而实例p的constructor属性也指向Object

如果不想丢失constructor,需要再加一句:

Person.prototype.constructor = Person;

2.4. 原型链

我们已经了解了原型prototype和隐式原型__proto__

一个实例,可以通过__proto__访问到父构造函数的原型;原型也可以通过__proto__访问到原型的原型。调用一个实例的方法时,会从实例本身开始查找这个方法。如果实例本身没有,会通过__proto__往父级查找,直到找到为止。如果找到终点也没找到,返回null

这个搜索的过程形成的链状关系就是原型链。

2.5. 小结

(1)对于对象:

对象拥有__proto__constructor属性,没有prototype

  • 对象.__proto__ === 父构造函数.prototype
  • 对象.constructor === 父构造函数.prototype.constructor。一般来说,父构造函数.prototype.constructor === 父构造函数本身;

(2)对于函数:

  • 作为构造函数,独有prototype属性,构造函数.prototype里有两个参数:constructor__proto__

    • 构造函数.prototype.constructor === 父构造函数.prototype.constructor。一般来说,父构造函数.prototype.constructor === 父构造函数本身;
    • 构造函数.prototype.__proto__ === 父构造函数.prototype(比如Date.prototype.__proto__ === Object.prototype
  • 函数也是对象,所以函数也拥有__proto__constructor属性,与(1)相同;

  • 注意:构造函数.prototype.__proto__构造函数.__proto__是不一样的。比如:

      Date.prototype.__proto__ === Object.prototype; //true
      // 因为原型的本质还是对象,即Date.prototype是一个对象; 且对象的父构造函数是Object。
      Date.__proto__ === Function.prototype; // true
      // 因为Date本身是构造方法,它的父构造函数是Function。
    

3)作用域链顶层为null

3. 原型链图

我们来看一个简单的栗子:

function Father() {};
const son = new Father();

你知道son和Father的prototype__proto__constructor是怎样吗?一起来画画吧~

第一步:prototype

这里有一个构造函数Father,我们可以通过Father.protorype访问到它的原型。

第二步:instance实例

  • 实例是new出来的对象,所以实例拥有__proto__constructor两个属性;
  • 实例对象没有prototype属性!(实例方法有==)

这里使用new操作符创建了一个实例对象son。

// 接上边栗子

// 实例对象没有prototype属性
son.prototype === undefined; // true

// 实例方法有prototype(这里的Father是Function的实例)
Father.prototype !== undefined; // true
Father.__proto__ === Function.prototype; // true

第三步:__proto__

实例的__proto__属性指向父构造函数的prototype:Father.prototype === son.__proto__

第四步:constructor

第五步:Father的长辈们

往Father的父级找上去,最后son、Father、Function、Object之间的原型链图是这样的:
(建议先看黑色的线,看完再看看亮色的)

自测

我们画完了原型链图,想必下边这些判断对你来说已经so easy~

// 对象的__proto__指向父级的prototype
son.__proto__ === Father.prototype; // true
// 对象的构造器指向父构造函数
son.constructor === Father; // true
// 构造函数.prototype.constructor指向构造函数本身,所以实例对象的构造器也指向:构造函数.prototype.constructor
son.constructor === Father.prototype.constructor; // true

// 构造函数.prototype.constructor指向它本身
Father.prototype.constructor === Father; // true
// 构造函数.prototype是一个对象,对象的父级是Object,所以Father.prototype.__proto__指向Object.prototype
Father.prototype.__proto__ === Object.prototype; // true
// 构造函数Father的父级是Function,所以Father.__proto__指向Function.prototype
Father.__proto__ === Function.prototype; // true
// 构造函数的构造器是它的父级函数,所以...
Father.constructor === Function; // true
// 1) 构造函数.prototype.constructor指向构造函数本身,所以Function.prototype.constructor === Function; 2) 构造函数的构造器就是它的父级,所以Function.constructor === Function; 3) 所以两者相等
Father.constructor === Function.prototype.constructor; // true

// Function自己也是它的父构造函数的实例。这里是因为:构造函数.prototype.constructor指向构造函数本身
Function.prototype.constructor === Function; // true
// Function.prototype是一个对象,对象的上级是Object,所以...
Function.prototype.__proto__ === Object.prototype; // true
// Function自己也是它的父构造函数的实例,但它的父构造函数还是Function,所以...
Function.__proto__ === Function.prototype; // true
// Function.__proto__指向Function.prototype,是一个对象,对象没有prototype属性,所以是undefined
Function.__proto__.prototype === undefined; // true
// Function.__proto__指向Function.prototype, 构造函数.prototype.constructor指向自身,所以...
Function.__proto__.constructor === Function; // true
// Function.__proto__指向Function.prototype, 是一个对象,这个对象是Object的实例,所以...
Function.__proto__.__proto__ === Object.prototype; // true
// Function.__proto__.__proto__指向Object.prototype,构造函数.prototype.constructor指向构造函数自身,所以...
Function.__proto__.__proto__.constructor === Object; //true
// Function.__proto__.__proto__指向Object.prototype,Object.prototype的上一层是原型链的终点,JS规定值为null
Function.__proto__.__proto__.__proto__ === null; //true

// Object也是一个构造函数,构造函数.prototype.constructor指向构造函数自身
Object.prototype.constructor === Object; // true
// Object.prototype的上一层是原型链的终点,即null
Object.prototype.__proto__ === null; // true
// Object还是它父构造函数的一个实例,所以Object.__proto__指向了Function.prototype
Object.__proto__ === Function.prototype; // true
// Object.__proto__指向它的父构造函数.prototype,是一个对象,所以父构造函数.prototype再上一层则指向了Object.prototype
Object.__proto__.__proto__ === Object.prototype; // true
// Object.__proto__指向它的父构造函数.prototype,构造函数.prototype.constructor指向自身,所以...
Object.__proto__.constructor === Function; // true
// 1)Object.__proto__指向父构造函数.prototype,是一个对象;2)对象.__proto__指向Object.prototype;3)Object.prototype.__proto__为终点null
Object.__proto__.__proto__.__proto__ === null; // true

4. 拓展

4.1. 构造函数

  • 从外观来看,构造函数跟普通函数其实没啥区别,但它可以用new关键字创建对象;
  • 一般来说,公共属性定义到构造函数里面,公共方法我们放到原型对象身上(原型上的方法才会被实例共享)。

4.1.1. 实例成员和静态成员

  • 实例成员:
    • 在构造函数内部,通过this添加的成员;
    • 只能通过实例化的对象来访问。
  • 静态成员:
    • 在构造函数本身上添加的成员;
    • 只能通过构造函数来访问。
function Person(name, age) {
  // 实例成员
  this.name = name;
  this.age = age;
}

// 静态成员
Person.sex = '女';

const p1 = new Person('LaoHuang', 24);
console.log(p1.name); // 'LaoHuang'
console.log(p1.age); // 24
console.log(p1.sex); // undefined (实例无法访问构造函数的静态成员)

console.log(Person.name); // Person undefined (构造函数无法直接访问实例成员,必须实例化后才能访问)
console.log(Person.sex); // 女 (构造函数可以访问它的静态成员)

4.1.2. new一个实例

这个过程就是实例化

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

const p1 = new Person('LaoHuang');
console.log(p1) // Person {name: "LaoHuang"}

p1就是一个实例对象。new一个新对象的过程,大概有以下几步:

  • 创建一个空对象p1: {};
  • 为p1准备原型链连接: p1.__protp__ = Person.prototype
  • 绑定this,使构造函数的this指向新对象p1:Person.call(this)
  • 为新对象的属性赋值:p1.name;
  • 返回this:return this。此时的新对象就拥有构造函数的方法和属性了。

4.1.3. 共享实例的方法

在构造函数的原型上添加的方法才会被实例共享。

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  console.log('名字:' + this.name);
}

const p1 = new Person('LaoHuang');
const p2 = new Person('FeiFei');
p1.getName(); // 名字:LaoHuang
p2.getName(); // 名字:FeiFei
console.log(p1.getName === p2.getName); // true

4.2. Class类

  • 类的本质还是一个函数,它就是构造函数的另一种写法;

  • 类没有变量提升,必须先定义,才能实例化;

  • constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,会默认添加一个空的constructor方法;

  • 类的所有方法都定义在它的prototype上:

    class Person {
      constructor(name) {
        this.name = name;
      }
      getName() {
        console.log('名字:' + this.name);
      }
    }
    
    const p1 = new Person('LaoHuang');
    const p2 = new Person('FeiFei');
    p1.getName(); // 名字:LaoHuang
    p2.getName(); // 名字:FeiFei
    console.log(p1.getName === p2.getName); // true
    

4.2.1. 如何向类中添加方法

使用Object.assign()

class Person {
  constructor(name, sex) {
    this.name = name;
    this.sex = sex;
  }
  getName() {
    console.log('名字:' + this.name);
  }
}

Object.assign(Person.prototype, {
  getSex(){
    console.log(this.name + '的性别是:' + this.sex)
  }
});

const p1 = new Person('LaoHuang', '女');
const p2 = new Person('FeiFei', '男');
p1.getSex(); // LaoHuang的性别是:女
p2.getSex(); // FeiFei的性别是:男
console.log(p1.getSex === p2.getSex); // true

4.3. 类和构造函数的区别

  • 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行;
  • 类的所有实例共享一个原型对象;
  • 类的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。

4.4. 继承

这里不展开讲继承,只涉及原型相关的。可以尝试着画一下原型图~

4.4.1. 构造函数+原型对象

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function () {
  console.log('名字:' + this.name);
};
function Girl(name, sex) {
  // 将实例化时,Person中的this指向当前实例
  Person.call(this, name);
  this.sex = sex;
}
// Girl.prototype = Person.prototype; // 用这种方式的话,给子类增加原型方法,同样会影响到父类
Girl.prototype = new Person();
Girl.prototype.sing = function () {
    console.log('I am singing');
};
let g1 = new Girl('小红', '女');
console.log(Person.prototype); // {getName: ƒ, constructor: ƒ}
console.log(Girl.prototype); // Person {name: undefined, sing: ƒ}

它的原型链图是这样的:

从这张图可以看出:

// 1)g1.constructor指向Girl.prototype.constructor
// 2)而Girl.prototype是Person的实例,且Girl.prototype.constructor相当于(new Person).constructor,即Person.prototype.constructor,即Person本身
g1.constructor === Person; //true

思考一下:如果想让g1.constructor === Girl,要怎么改呢?

4.4.2. extends语法糖

(1)super:

super可作为函数和对象使用:

  • 当作为函数使用时,只可在子类的构造函数中使用,表示父类的构造函数,但是 super中的this指向的是子类的实例,因此在子类中super()表示的是Parent.prototype.constructor.call(this)
  • 当作为对象使用时,super表示父类原型对象,即Parent.prototype

(2)extends干了啥:

  • 第一步:继承父类的原型,将子类的__proto__指向父类本身;
  • 第二步:call继承,就是super()的处理过程。把父类的对象方法继承给子类对象;这也是为什么在es6的继承时必须要加上super(),因为不加的话无法继承到父类的对象属性。
  • 第三步:创建子类自己的方法。
class Person {
  constructor(name){
    this.name = name;
  }
  getName(){
    return this.name;
  }
}
class Girl extends Person{
  constructor(name, sex){
    super(name);
    this.sex = sex;
  }
  getSex(){
    return this.getName() + '的性别:' + this.sex;
  }
}
const g1 = new Girl('小红', '女');
console.log(g1.getSex()); // 小红的性别:女

它的原型链图是这样的(记得与第一种继承比较一下):

从这张图可以看出:

// 这是extends特殊的地方
Girl.__proto__ === Person; //true

5. 参考