前言
prototype
对我来说一直都是比较难理解的东西,它给我的感觉就像凭空冒出来的一样,又是面试出现频率很高的问题,每次都是死记硬背式的去理解,结果根本没学会什么,一段时间之后又会忘记。
就像我一开始看到这种图,是有点晕的:
这篇我想分享下我学习原型
的过程和收获,尽量使用简单的大白话,争取让每个看到这篇文章的新手能最快的理解
原型 & 原型链
prototype
prototype: object that provides shared properties for other objects
从这句话可以知道
prototype
它就是一个对象!(指向原型)- 它用于连接两个对象之间的属性,实行共享。
你可以把 prototype
比作婴儿身上的脐带, 连接了母亲和孩子,孩子可以汲取母亲的营养长大, balabala....
talk is simple, see my code:
// 创建 Child 构造函数
function Child() {
}
// prototype是函数才会有的属性
Child.prototype.name = 'fuck';
var child = new Child();
console.log(child.name) // fuck
现在把以前的理解全部扔掉,我们就认为prototype
是一个对象,把Child.prototype = { ... }
就看成是obj.a = { .... }
, 而 prototype
这个对象它是指向了构造函数而创建的实例的原型。也就是child
的原型. 也就是说 Child
和 child
就是孩子, 母亲就是 原型(Child.prototype 指向的对象)
, 通过prototype
以及 下面说的 __proto__
连接了它们的原型。
那么~ 到底什么是原型。你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
__proto__
Every object has an implicit reference (called the object's prototype)
就是说 __proto__
是一个隐式引用, 它也是指向了对象的原型,看下面的 code:
function Child() {}
var child = new Child();
console.log(child.__proto__ === Child.prototype); // true
可能这个还不能完全搞懂隐式的意思, 那我们先看下这张图
我们创建obj
对象,但展开之后发现多了一个 __proto__
属性,这意味着 obj
被隐式地挂载了另一个对象的引用,置于 __proto__
属性中。
也就是说,所谓的隐式,是指不是由开发者(你和我)亲自创建/操作。
注意:
- 可以通过
Object.getPrototypeOf(obj)
间接访问指定对象的prototype
对象。
console.log(Object.getPrototypeOf(child) === Child.prototype) // true
- 通过
Object.setPrototypeOf(obj, anotherObj)
间接设置指定对象的prototype
对象。
constructor
每个原型都有一个 constructor
属性指向关联的构造函数。
function Person() {
}
console.log(Person === Person.prototype.constructor); // true
这里有一个东西需要注意:
function Person() {
}
var person = new Person()
console.log(person.constructor === Person); // true
为什么是 true
呢? 其实就是因为 person
的隐式引用, 如下图
person.constructor -> person.__proto__.constructor -> Person.prototype.constructor === Person
到这里,应该就对最上面那张图的最上面一部分理解了
原型链
a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain.
就是说, 一个原型对它的原型可能都有一个隐式的引用,就是说 prototype
对象也有自己的prototype
, 再简单了说就像是个族谱,我要找你的祖宗十八代,需要从你爸爸开始,你爸爸也有爸爸,爸爸的爸爸也有爸爸, balabala..., 最后找到了你的祖宗十八代。
说人话其实就是: 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。下面看个例子:
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin 当读取实例的属性找不到 name, 就去 Person.prototype 中找
- 这个时候就会问了,如果在原型里还找不到,那原型的原型又是什么?其实我们打印出
person
就可以知道
prototype
就是个对象,既然是对象,那它就是通过 Object
构造函数生成的,结合之前所讲,实例的 __proto__
指向构造函数的 prototype
, 所以
person.__proto__.__proto__ === Object.prototype // true
- 而问题又来了, 我怎么知道我找的是祖宗十八代呢,不是使其代或者十九代。
看到上面的简介,non-null
, 表示我找到原型为null
之后停止
Object.prototype.__proto__ === null
这一路查找的路径所连成的线,就是原型链。
继承
Object.create & new
在学习如何继承之前,我们要先了解两个概念
Object.create()
var obj ={a: 1}
var b = Object.create(obj)
console.log(obj.a) // 1
console.log(b.a) // 1
b.a = 2
console.log(obj.a) // 1
从上面的例子就可以看出Object.create
解决了对象的拷贝问题,它创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
, 其实内部就是
function create(o) {
let F = function () {}
F.prototype = o
return new F()
}
new
new
一直面试问的很多的问题,它具体做了什么呢?
- 创建了一个全新的对象。
- 这个对象会被执行
[[Prototype]]
(也就是__proto__
)链接, 链接到这个函数的prototype
对象上。- 生成的新对象会绑定到函数调用的
this
。- 如果函数没有返回对象类型
Object(包含Functoin, Array, Date, RegExg, Error)
,那么new
表达式中的函数调用会自动返回这个新的对象。
转化成代码就是
function create() {
// 1. 创建空对象
let obj = {}
// 截取数组,并获取第一个参数
let Con = [].shift.call(arguments)
// 2. 链接到原型
obj.__proto__ = Con.prototype
// 3. 绑定 this
let result = Con.apply(obj, arguments)
// 4. 返回
return result instanceof Object ? result : obj
}
那我们现在借用 Object.create
的特性,可以更改为:
function create() {
let Con = [].shift.call(arguments)
let obj = Object.create(Con.prototype)
let result = Con.apply(obj, arguments)
return result instanceof Object ? result : obj
}
为什么要说 new
呢?因为在我的理解中,new
方法其实就像是一种继承。
继承历程
首先说下这两种方法
Object.setPropertyOf
,给我两个对象,我把其中一个设置为另一个的原型。Object.create
,给我一个对象,它将作为我创建的新对象的原型。
如何使用,上面都有介绍,不在赘述。
下面介绍下我们日常工作中,实现继承的一路历程。首先我们创建一个构造函数
function SuperPerson (phone) {
this.name = 'xiatian'
this.age = 24
this.hobby = ['lady', 'female', 'woman', 'girl']
this.phone = phone
}
SuperPerson.prototype.getName = function() {
return this.name
}
类式继承
// 声明子类
function SubPerson (phone) {
this.sex = 'male'
}
// 继承父类
SubPerson.prototype = new SuperPerson()
类式继承的原理就是将子类的原型指向父类的实例,这样子类的prototype
就可以访问到new Person()
的__proto__
, 从而访问到它的属性和方法。
但是,这样的继承是有两个问题的:
- 如果父类的属性里有引用类型,那么就会在子类的所有实例里共用
var instance1 = new SubPerson()
var instance2 = new SubPerson()
console.log(instance2.hobby) // ['lady', 'female', 'woman', 'girl']
instance1.hobby.push('man')
console.log(instance2.hobby) // ['lady', 'female', 'woman', 'girl', 'man']
- 如果子类有参数,则无法传递给父类,实现初始化。如此🌰,
phone
无法传递给父类
function SuperPerson (phone) {
this.name = 'xiatian'
this.age = 24
this.hobby = ['lady', 'female', 'woman', 'girl']
this.phone = phone
}
// 声明子类
function SubPerson (phone) {
this.sex = 'male'
}
// phone 无法传递
SubPerson.prototype = new SuperPerson()
构造函数继承
为了解决类式继承的问题,使用了构造函数式继承
function SuperPerson (phone) {
this.name = 'xiatian'
this.age = 24
this.hobby = ['lady', 'female', 'woman', 'girl']
this.phone = phone
}
// 声明子类
function SubPerson (phone) {
this.sex = 'male'
// 重点!!
// call 绑定this, 并把 phone 传递
SuperPerson.call(this, phone)
}
但这也存在问题,prototype
没有涉及也就无法继承,除非把他们都写在构造函数中,但这样每个实例都只此一份,无法共享
组合继承
顾名思义,就是组合了类式继承和构造函数式继承
function SuperPerson (phone) {
this.name = 'xiatian'
this.age = 24
this.hobby = ['lady', 'female', 'woman', 'girl']
this.phone = phone
}
SuperPerson.prototype.getName = function() {
return this.name
}
// 声明子类
function SubPerson (phone) {
this.sex = 'male'
SuperPerson.call(this, phone)
}
// 类式继承
SubPerson.prototype = new SuperPerson()
var instance1 = new SubPerson()
var instance2 = new SubPerson()
instance1.hobby.push('man')
console.log(instance1.hobby) // ['lady', 'female', 'woman', 'girl', 'man']
console.log(instance2.hobby) // ['lady', 'female', 'woman', 'girl']
但即使这样,还是存在问题的,它调用了两次父类的构造函数,导致父类构造函数中的属性重复的两次, 如图所示
寄生式继承
为了消除组合继承的问题,我们只需要继承父类的prototype
, 这就要用到上面说的Object.create()
SubPerson.prototype = Object.create(SuperPerson.prototype)
// 这样也是有个问题,SubPerson.prototype.constructor !== SubPerson
, 因为对照Object.create
的内部代码
function create(o = 'SuperPerson.prototype') {
let F = function () {}
F.prototype = o ('SuperPerson.prototype')
return new F() ('SubPerson.prototype')
}
// 所以
// SubPerson.prototype.constructor
// = F.prototype.constructor
// = SuperPerson.prototype.constructor
// = SuperPerson
所以我们要改造一下
function inherit(subClass, superClass) {
var p = Object.create(superClass.prototype)
p.constructor = subClass
subClass.prototype = p
}
寄生式组合继承
到这里就很简单了, 跟上面的一起连起来
function SuperPerson (phone) {
this.name = 'xiatian'
this.age = 24
this.hobby = ['lady', 'female', 'woman', 'girl']
this.phone = phone
}
SuperPerson.prototype.getName = function() {
return this.name
}
// 声明子类
function SubPerson (phone) {
this.sex = 'male'
SuperPerson.call(this, phone)
}
// 寄生式继承
inherit(SubPerson, SuperPerson)
// 添加子类 prototype
SubPerson.prototype.getSex = function() {
return this.sex
}
var instance = new SubPerson('13776020154')
instance.getName() // 'xiatian'
instance.phone // '13776020154'
instance.getSex() // 'male'