本文从
prototype
/__proto__
/[[prototype]]
切入,理清实例,构造函数,原型之间的关系之后,列举创建对象的方法,分析继承的原理及方法
实例/构造函数/原型
首先,先祭出神图(第一列为实例,第二列为构造函数,第三列为原型)
补充说明:
- 每个函数都有
prototype
属性,该属性指向原型,原型也是一个对象。 - 每个对象(除了
Object.prototype
)都有[[prototype]]
属性,指向了创建该对象的构造函数的原型。但是由于[[prototype]]
属性是内部属性,我们并不能访问到,所以使用__proto__
来访问。 __proto__
将对象连接起来组成了原型链。 对象可以通过__proto__
来寻找不属于该对象的属性.- 函数的
prototype
属性指向它的原型,函数原型的constructor
属性指向该函数 - 实例的
__proto__
指向它的构造函数的原型 - 函数的
__proto__
指向Function
的原型(每一个构造函数都是Function
的实例) - 函数的原型的
__proto__
都指向Object
的原型,Object
的原型是null(原型链的最顶端) - 实例可以通过原型链找到本身不存在的属性和方法 (比如
f1
沿着__proto__
可以找到construtor
属性 :f1.constructor = Foo
)
创建对象(红宝书4th)
- 工厂模式
function createPeason(age,name,job){
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){console.log(this.name)}
return o;
}
const p1 = createPeason(18,"ss","IT")
//存在的问题: 没有指定原型
- 构造函数模式
function Person(age,name,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){console.log(this.name)}
}
const p1 = new Person(18,"SS","IT")
//存在的问题: 定义的方法会在每个实例上都创建一遍
与工厂模式的区别:
- 没有显示的创建对象;
- 属性和方法直接赋值给了this;
- 没有return
回顾一下new的原理
- 在内存中创建一个新对象;
- 这个新对象内部的
[[prototype]]
属性被赋值为构造函数的prototype
属性 - 构造函数内部的this被赋值为这个新对象(即this指向新对象),并执行构造内部代码(给新对象添加属性和方法)
- 如果构造函数返回非空对象,则返回该对象,扔掉刚创建的新对象,否则,返回刚创建的新对象。
注意: 构造函数和普通函数唯一的区别就是调用方式不同,任何函数只要使用new操作符调用就是构造函数,不使用new操作符调用就是普通函数。
//如果上面的Person采用如下方式调用,就是在window全局对象上添加属性和方法
Person(18,"SS","IT")
window.sayName() // SS
- 原型模式
funciton Person(){};
Person.prototype.name = "SS";
Person.prototype.age = 18;
Person.prototype.job = "IT";
Person.prototype.sayName = function(){console.log(this.name)}
const p1 = new Person();
const p2 = new Person();
p1.sayName() // "SS"
p2.sayName() // "SS"
//属性和方法由所有的实例共享
p1.name = "SSS"
p1.sayName() // "SSS"
p2.sayName() // "SS"
//实例上的属性会遮蔽原型上的同名属性
继承(红宝书4th)
- 原型链
function Super(){}
function Sub(){}
//继承Super
Sub.prototype = new Super();
const ins = new Sub();
console.log(ins instanceof Super) //true
// 问题: ins.constructor = Super
原型链的问题:
- 原型中包含的引用值会在实例之间共享
- 子类型在实例化时不能给父类型的构造函数传参
- 盗用构造函数
为了解决原型包含引用值导致的继承问题,出现了盗用构造函数方式的继承,原理是 : 在子类构造函数中调用父类构造函数。
function Super(){
this.colors = ['red','yellow']
}
function Sub(){
//继承Super
Super.call(this)
//Super构造函数在为Sub的实例创建的新对象的上下文中执行了。
//相当于新的Sub对象上运行了Super函数中所有的初始化代码,结果就是每个实例都会有自己的colors属性
}
const ins1 = new Sub();
ins1.colors.push('white')
const ins2 = new Sub();
ins2.colors.push('black')
console.log(ins1.colors) // 'red','yellow','white'
console.log(ins2.colors) // 'red','yellow','black'
优点
- 可以在子类构造函数中向父类构造函数传参
缺点
- 必须在构造函数中定义方法,不能复用
- 子类不能访问父类原型上的方法,因为没有形成原型链
- 组合继承
综合原型链和盗用构造函数,将两者的优点集中起来。思路:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。
function Super(name){
this.name = name;
this.colors = ['red','yellow']
}
Super.prototype.sayName = function(){
return this.name
}
function Sub(name,age){
//继承属性
Super.call(this,name) //1
this.age = age
}
//继承方法
Sub.prototype = new Super(); //2
缺点:
- 两次调用父类构造函数,使得父类的属性/方法重复定义
- 原型式继承
创建一个临时构造函数,并将传入的对象赋值给这个构造函数的原型。
适用场景: 不需要单独创建构造函数,但仍然需要在对象间共享信息
function newObject(o){
function F(){};
F.prototype = o;
return new F();
}
const person = {
name : "ss",
age : 18,
colors : ['red','yellow']
}
const person2 = newObject(person);
person2.name = "sss";
person2.colors.push('white');
const person3 = newObject(person);
person3.name = "ssss";
person3.colors.push('black');
console.log(person.colors) // 'red','yellow','white','black'
//问题: person相当于是person2和person3的原型,所以person的属性和方法在person2和person3中是共享的
这里的功能类似于Object.create()
const person = {
name : "ss",
age : 18,
colors : ['red','yellow']
}
const person2 = Object.create(person);
person2.name = "sss";
person2.colors.push('white');
const person3 = Object.create(person);
person3.name = "ssss";
person3.colors.push('black');
console.log(person.colors) // 'red','yellow','white','black'
缺点:
- 在对象间共享属性和方法
- 寄生式继承
思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
适用场景: 主要关注对象,而不在乎类型和构造函数
function createAnotherPerson(original){
const clone = newObject(original); // 通过调用函数创建一个对象
clone.sayHi= function(){ //增强对象
console.log("hi")
}
return clone //返回对象
}
const person1 = createAnotherPerson(person)
//基于person返回了一个新对象,person1具有person的所有属性和方法,还有一个新方法sayHi
缺点:
- 函数难以复用,与构造函数模式类似
- 寄生式组合继承 [引用类型继承的最佳模式]
为解决组合继承中两次调用重复的问题,有了寄生式组合继承。基本思路: 不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。即使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类型。
function inheritPrototypr(sub,super){
const prototype = newObject(super.prototype) //以super.prototype为原型创建对象,创建的对象只有原型上的属性和方法
prototype.constructor = sub; //增强,指定
sub.prototype = prototype //赋值,形成原型链
}
function Super(){
this.name = name;
this.colors = ['red','yellow']
}
Super.prototype.sayName = function(){
return this.name
}
function Sub(name,age){
Super.call(this,name); //继承Super的属性和方法 1
this.age = age
}
inheritPrototypr(Sub,Super); //继承
Sub.prototype.sayAge = function(){
return this.age;
}
优点: 只调用了一次Super的构造函数,并保留了原型链。
instanceof
操作符
instanceof
用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。
function A(){};
function B(){};
A.prototype = new B();
const c = new A();
console.log( c instanceof B ) //true
// c实例的原型链由new B()对象,B的原型,Object的原型组成
//在检查 c instanceof B 时,JavaScript引擎查找B的原型,是否存在于c实例的原型链上
参考资料
js [[Prototype]]、proto 和Prototype
你不知道的JavaScript
JavaScript高级程序设计 4th
JavaScript忍者秘籍 2th