深刻原型

原型是个玄学,学了好久的js都没有搞清楚究竟是个啥,老是只知其一;不知其二,今天系统的总结下(按我的理解),欢迎批评指正。
说到原型,那么咱们搞清楚这些名词先:构造函数、原型、实例、__proto__、new操做等。html

1. es5 构造函数(类)

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

// 构造函数
function Foo(name) {
    // 私有属性
    var age = 1
    // 公有属性
    this.name = name
}
// 原型上的属性
Foo.prototype.getName = function() {
    return this.name
}
// 静态属性
Foo.id = 123

// 实例foo
var foo = new Foo('Tom')
foo.name // Tom
foo.age // undefined
foo.getName() // Tom
Foo.id // 123
复制代码
  • 问:为何foo能访问到name、getName, 访问不到age?
    答:es6

    1. 首先搞清楚 new 操做干了哪些事情? 1. 开辟一个对象obj,2. obj.__proto__ = Foo.prototype, 3. 强制改变this。
    // 模拟 new 操做
    function myNew(Foo){
        var obj = {}
        // 解释为何能访问 getName
        obj.__proto__ = Foo.prototype
        // 将构造函数里的公有属性,强制绑定到obj, 解释问什么能访问 name
        Foo.call(obj)
        return obj
    }
    复制代码
    1. 访问规则:当 foo 访问某属性时,首先会去寻找foo对象自己是否存在改属性,若存在,直接返回;若不存在,则根据__proto__的指向去寻找直到指向null。
    2. 结论:能够访问到name、getName, 而访问不到age、id。id 是静态属性,直接经过构造函数名访问Foo.id。

2. es5 继承

其基本思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。设计模式

2.1 类式继承, 也叫原型链继承

实例 foo 能访问到构造函数 Foo 里的公有属性和原型上的属性, 实例 parent 能访问到构造函数 Parent 里的公有属性和原型上的属性, 将构造函数Foo的原型指向实例parent,则 foo 也能够访问 parent 所能访问的内容, 从而实现继承。bash

function Parent(){
    this.aaa = 'aaa'
    this.books = ['1', '2']
}
Parent.prototype.getAAA = function(){
    return this.aaa
}
// 这个时候会覆盖以前的 Foo.prototype.getName, 解决:子类原型上自定义的方法后移
Foo.prototype = new Parent()

Foo.prototype.getName = function() {
    return this.name
}

var foo = new Foo('Tom')
foo.getAAA() // 'aaa'
复制代码

原型关系:app

缺点:1. 父类Parent里的公有引用数据类型属性,会互相影响;2. 没法向父类传参。

var foo1 = new Foo('Tom')
var foo2 = new Foo('Tom1')
foo1.books.push('3')
foo2.books // ['1','2','3']
复制代码

2.2 构造函数继承

解决父类Parent里的公有引用数据类型属性,会互相影响
缺点:没法访问Parent.prototype上的内容。函数

function Parent(name){
    this.books = ['1','2']
}
function Foo(name) {
    Parent.call(this,name)
}
var foo1 = new Foo('Tom1')
var foo2 = new Foo('Tom2')
foo1.books.push('3')
foo2.books // ['1','2']
复制代码

2.3 组合继承

Parent里的公有引用数据类型属性互不影响,也可访问Parent.prototype上的内容。
缺点:要调用两次父类构造函数,而且books会存在于foo和Foo.prototype上性能

function Parent(){
    this.books = ['1','2']
}
function Foo() {
    Parent.call(this) // 第二次调用 new Foo() 时。
}
// 子类的原型指向父类的实例
Foo.prototype = new Parent() // 第一次调用。
var foo = new Foo()
复制代码

原型关系: ui

2.4 原型式继承

借助第三方构造函数F实现继承, 本质上是经过__proto__牵桥搭线。this

function inherit(o) {
    function F(){}
    F.prototype = o
    return new F()
}
obj = {
    age: 10
}
me = inhreit(obj) // me.__proto__指向F.prototype也就是o

// 等同于
const me = Object.create(obj);
// Object.create原理
const me = Object.create(obj); ===> me.__proto__ = obj
复制代码

2.5 寄生组合式继承

经过借用构造函数来继承属性(call),经过原型链来继承方法。相比较组合继承,则其基本思路是:没必要为了指定子类的原型而调用父类的构造函数(避免调用两次父类构造函数),而是将父类原型的副本放到子类原型上。es5

function inherit(o) {
    function F(){}
    F.prototype = o
    return new F()
}
function inheritPrototype(Child, Parent) {
    var p = inherit(Parent.prototype) // p.__proto__ = Parent.prototype
    p.constructor = Child
    Child.prototype = p
}

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

inheritPrototype(Child, Parent)
// 为避免被覆盖,定义子类原型上的方法,要写在 inheritPrototype 以后
Child.prototype.getAge = function() {
    return this.age
}
var child = new Child('Tom', 12)
child.getAge() // 12
复制代码

以上全部继承方式,原型上的引用数据类型被更改时会互相影响。 使用约定 --- 通常原型上只用来存方法,而不存数据,来规避。

3. es6 类 class

3.1 类的全部方法都定义在类的prototype属性上面。

// es6
class Point {
    // 静态属性
    static id = 1
    
    // 私有属性,约定用_加以区分,但实例仍然能够访问
    _count = 1

    // 原型上的方法
    constructor() {
        // 公有属性
        this.x = 1
    }
    
    toString() {
    // ...
    }
    
    toValue() {
    // ...
    }
}
const point = new Point()
point.hasOwnProperty('x') // true
point.hasOwnProperty('_count') // true

// class里的方法 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

// es5 的方法
function Point() {
    this.toValue = function() {}
}
// toValue方法在实例 point 上,而不是 Point.prototype 上。
var point = new Point()
复制代码

3.2 父类的静态方法,能够被子类继承。

相比于es5, es6多作了这步操做Child.__proto__ = Parent(子类的__proto__指向父类)。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'
Bar.__proto__ === Foo // true
复制代码

4. es6 继承

4.1 super

  • 做为方法
    1. 表明父类的构造函数,至关于A.prototype.constructor.call(this)(A是父类),
    2. 子类必须调用super方法,而且是在constructor方法中,不然新建实例时会报错。
  • 做为对象
    1. 指向父类的原型对象,父类实例上的方法或属性,是没法经过super调用的。
    2. 在子类普通方法中经过super调用父类的方法时,父类方法内部的this指向当前的子类实例。
  • 用在静态方法之中 && 做为对象
    1. 这时super将指向父类,而不是父类的原型对象。
    2. 在子类的静态方法中经过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

ES5 的继承,实质是先创造子类的实例对象this(肯定this指向),而后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制彻底不一样,实质是先将父类实例对象的属性和方法,加到this上面(因此必须先调用super方法),而后再用子类的构造函数修改this(肯定this指向)。

4.2 类的 prototype 属性和 proto 属性

  • 子类的__proto__属性,老是指向父类。(能够解释 static 属性能被继承)
  • 子类prototype属性的__proto__属性,表示方法的继承,老是指向父类的prototype属性。
class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true


// 等同于
class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype = Object.create(A.prototype)

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
// 等同于
B = Object.create(A)

const b = new B();

// ------------------------------------------------------
// setPrototypeOf 原理
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
复制代码

5. 总结

  • js 能访问到属性、方法的是经过 __proto__ 一层一层的向上查找,直到null。
  • 类实例的__proto__指向类的原型(foo.__proto__ = Foo.prototype), 由于new操做的原理。
  • js 继承的核心思路是子类原型上的__proto__指向父类原型(访问方法) && call强制改变this来实现互不影响(访问属性)。

参考文献

js设计模式-张容铭
js高程-第三版
es6入门-阮一峰

相关文章
相关标签/搜索