JavaScript学习笔记(三)原型和原型链

1. 原型和原型链

ECMAScript实现的是基于原型和原型链的继承。对于任何对象,都有一个__proto__属性指向其原型对象,原型对象就是这个对象的构造函数的prototype属性,同时原型对象的constructor属性又指向构造函数,看下面这个例子:

function People(name) {
    this.name = name
}
let xiaoming = new People('xiaoming')

xiaoming.__proto__ === People.prototype // true
xiaoming.__proto__
/* {constructor: ƒ}
constructor: ƒ People(name)
__proto__: Object*/
People.prototype.constructor === People // true
xiaoming.constructor === People // true

上面例子中,xiaoming.__proto__就是xiaoming这个对象的原型对象,也就是xiaoming的构造函数Peopleprototype属性。原型对象的constructor属性又指向了xiaoming对象的构造函数People

由于原型对象本身也是一个对象,因此原型对象也有其自身的__proto__属性,指向原型对象的构造函数的prototype属性,这样形成了一条由构造函数的prototype属性构成的原型链。

当访问对象属性或者调用对象方法时,首先在对象本身寻找;如果对象本身不具备所访问的属性或所调用的方法,就沿着原型链向上一级构造函数的prototype属性(原型对象)中寻找,一直到Object.prototype;如果Object.prototype上也没有找到,就会报错。

Object.prototype作为一个对象,其__proto__属性就是null,因此可以认为null是原型链的终点:

Object.prototype.__proto__ === null // true

2. 继承

在理解了原型和原型链之后,再来看ECMAScript中的继承就比较容易清晰了。ES5中,标准的继承分为三步:

  • 在子类(构造函数,为了便于理解也称为“类”,但ES本身没有类的概念)中调用父类的构造函数(通过callapply方法将构造函数中this的指向子类)
  • 将子类的原型对象与父类的原型对象关联
  • 将子类的构造函数与自身关联

下面逐步分析继承的过程。

2.1 基于构造函数的继承

首先是最简单的继承方式,也就是在子类中调用父类的构造函数:

// 最简单的继承:在子类中调用父类的构造函数
// 定义父类People
function People(name) {
    this.name = name
    this.say = function() {
        console.log(`My name is ${this.name}`)
    }
}
People.prototype.walk = () => {
    console.log('I am walking ...')
}
// 定义子类Student
function Student(name, age) {
    People.call(this, name)
    this.age = age
    this.study = () => {
        console.log('I am studying ...')
    }
}
// 实例化子类的对象
let xiaoming = new Student('xiaoming', 20)
xiaoming.name // "xiaoming"
xiaoming.age // 20
xiaoming.study() // I am studying ...
xiaoming.walk() // Uncaught TypeError: xiaoming.walk is not a function

这种方式虽然简单,但存在一个问题:子类只能继承父类的静态属性(如例子中父类Peoplename)和方法(如例子中People中定义的say),但不能继承父类原型对象上的属性和方法(如例子中People.prototype上的walk)。

2.2 基于构造函数和原型的继承

为了解决上述问题,还需要将子类和父类的原型相关联,可以通过以下2种方式实现:

// 让子类的prototype属性等于父类的prototype属性
Student.prototype = People.prototype

这种方式存在一个明显的问题:子类和父类共用一个原型对象,进而共用一个构造函数,就无法用instanceof运算符判断一个对象到底是由子类还是父类的构造函数产生。

更合理的方式为:

// 通过一个父类的实例对象将子类和父类的prototype关联
Student.prototype = Object.create(People.prototype)
// 或者写成下面这种形式
//Student.prototype = new People()

到这里虽然解决了子类和父类共用一个原型对象的问题,但子类此时并没有自己的构造函数,因此还需要为子类指定构造函数:

Student.prototype.constructor = Student

这样就完成了标准的ECMAScript的继承。

2.3 ES6的类和继承

ES6新增了关键字classextends,本质上是对ES5继承相关语法的语法糖:

class People {
    constructor(name) {
        this.name = name
    }
    eat() {
        console.log('I am eating ...')
    }
}
People.prototype.walk = () => {
    console.log('I am walking ...')
}

class Student extends People {
    constructor (name, age) {
        super(name) // 调用父类构造函数,等价于People.call(this, name)
        this.age = age
    }
    // 父类的静态方法自动被继承
    study() {
        console.log('I am studying ...')
    }
}

let xiaoming = new Student('xiaoming', 20)
xiaoming.name // "xiaoming"
xiaoming.age // 20
xiaoming.study() // I am studying ...

// 父类的静态方法和原型方法都被子类继承
xiaoming.eat() // I am eating ...
xiaoming.walk() // I am walking ...

// 子类的构造函数也被重新指定为子类自身
xiaoming.constructor === People // false
xiaoming.constructor === Student // true

可以看到,ES6的语法更加简洁,但本质上还是基于构造函数和原型链的继承。

3. new运算符

ECMAScript中对象可以通过new运算符作用在任意函数上产生。通常为了强调,将这些函数的首字母大写并称之为“构造函数”。new运算符的执行过程也可以通过原型和原型链来加以理解:

  • 第一步:创建一个空对象,并将空对象的原型对象(__proto__属性)与构造函数的原型对象(prototype属性)关联,从而使得对象可以访问构造函数原型链上的属性和方法
  • 第二步:执行构造函数,并将构造函数的this指向上一步创建的对象,从而使得对象可以访问构造函数的静态属性和方法
  • 第三步:如果构造函数自身返回一个对象,就用返回的对象取代前两步创建的对象;否则,直接返回前两步创建的对象

可以将new运算符用一个函数来模拟:

// 自定义函数模拟new执行过程
function myNew(constructor, [...rest]) {
    // 第一步,创建空对象并指定__proto__属性
    const obj = {}
    obj.__proto__ = Object.create(constructor.prototype)
    
    // 第二步,执行构造函数
    const temp = constructor.apply(obj, rest)
    
    // 第二步,判断构造函数有无返回值
    if (temp && (typeof temp === 'object') && (temp instanceof Object)) {
        obj = temp
        obj.__proto__ = Object.create(constructor.prototype)
    }
    
    return obj
}

function People(name, age) {
    this.name = name
    this.age = age
    this.eat = function() {
        console.log('I am eating ...')
    }
}

let xiaoming = myNew(People, ['xiaoming', 20])
xiaoming.name // "xiaoming"
xiaoming.age // 20
xiaoming.eat() // I am eating ...

这就实现了一个简单的模拟new运算符的方法。

你可能感兴趣的:(JavaScript学习笔记(三)原型和原型链)