原型是个玄学,学了很久的js都没有搞清楚到底是个啥,总是一知半解,今天系统的总结下(按个人理解),欢迎批评指正。
说到原型,那么我们搞清楚这些名词先:构造函数、原型、实例、__proto__、new操作等。
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?
答:- 首先搞清楚 new 操作干了哪些事情? 1. 开辟一个对象obj,2. obj.__proto__ = Foo.prototype, 3. 强制改变this。
// 模拟 new 操作 function myNew(Foo){ var obj = {} // 解释为什么能访问 getName obj.__proto__ = Foo.prototype // 将构造函数里的公有属性,强制绑定到obj, 解释问什么能访问 name var result = Foo.call(obj) var isObject = typeof result === 'object' && result !== null return isObject ? result : obj }- 访问规则:当 foo 访问某属性时,首先会去寻找foo对象本身是否存在改属性,若存在,直接返回;若不存在,则根据__proto__的指向去寻找直到指向null。
- 结论:可以访问到name、getName, 而访问不到age、id。id 是静态属性,直接通过构造函数名访问Foo.id。
- 首先搞清楚 new 操作干了哪些事情? 1. 开辟一个对象obj,2. obj.__proto__ = Foo.prototype, 3. 强制改变this。
2. es5 继承
其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
2.1 类式继承, 也叫原型链继承
实例 foo 能访问到构造函数 Foo 里的公有属性和原型上的属性, 实例 parent 能访问到构造函数 Parent 里的公有属性和原型上的属性, 将构造函数Foo的原型指向实例parent,则 foo 也可以访问 parent 所能访问的内容, 从而实现继承。
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'
原型关系:
缺点: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()
原型关系:
2.4 原型式继承
借助第三方构造函数F实现继承, 本质上是通过__proto__牵桥搭线。
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 = {}; me.__proto__ = obj
2.5 寄生组合式继承
通过借用构造函数来继承属性(call),通过原型链来继承方法。相比较组合继承,则其基本思路是:不必为了指定子类的原型而调用父类的构造函数(避免调用两次父类构造函数),而是将父类原型的副本放到子类原型上。
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 {
// 静态属性, stage-3 提案
static id = 1
// 静态方法,如果静态方法包含this关键字,这个this指的是类,而不是实例。
static getId() {
return this.id
}
// 私有属性,约定用_加以区分,但实例仍然可以访问
_count = 1
// 实例属性,count仍然会绑定到实例上(仍然可以通过 this.count 访问)
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
- 作为方法:
- 代表父类的构造函数,相当于
A.prototype.constructor.call(this)(A是父类), - 子类必须调用super方法,并且是在constructor方法中,否则新建实例时会报错。
- 代表父类的构造函数,相当于
- 作为对象:
- 指向父类的原型对象,父类实例上的方法或属性,是无法通过super调用的。
- 在子类普通方法中通过super调用父类的方法时,父类方法内部的this指向当前的子类实例。
- 用在静态方法之中 && 作为对象,
- 这时super将指向父类,而不是父类的原型对象。
- 在子类的静态方法中通过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来实现互不影响(访问属性)。