代码已经关联到github: 链接地址 文章有更新也会优先在这,觉得不错可以顺手点个star,这里会持续分享自己的开发经验(:
JavaScript是弱对象的语言,但是也有面向对象编程的封装与继承的方法,而原型和他们有密切的联系。
原型和原型链
了解原型和原型链
让我们从一个例子开始:
function Foo(){
}
const f1 = new Foo()
console.log(Foo.prototype===f1.__proto__)//?
这里的prototype
和__proto__
都是什么?输出的内容又是什么?
原型(prototype)
prototype
,它是函数所独有的属性,也就是Function.prototype
,从一个函数指向一个对象。它的含义是函数通过 new
关键字构造的实例的原型对象(可以理解为所有该函数的实例的公有属性),也就是这个函数所创建的实例的原型对象。
原型链
要了解原型链,首先要了解__proto__
,它是对象所独有的,__proto__
属性值都指向一个对象,就是父对象构造函数的prototype
(也就是我们通常说的原型),是对象获取其原型的浏览器实现,目前正式的规范是通过Object.getPrototypeOf()
获取,所以我们上面的例子打印的是true
。
它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__
属性所指向的父对象里找,如果父对象也不存在这个属性,则继续往父对象的__proto__
属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找….直到Object.prototype.__proto__
,也就是null
,而这一链式关系,就被我们成为原型链:
上图比较包含了完整的原型和构造函数关系,我们可以仅掌握f1
的原型链:
f1.__proto__ === Foo.prototype
Foo.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
小结一下:
- 只有函数有
prototype
,函数通过 new 关键字构造的实例的原型对象(可以理解为所有该函数的实例的公有属性) - 只要是对象就有
__proto__
且其指向构造函数的prototype
, 还有constructor
- 原型链就是一条由
__proto__
指向构造的查找对象属性的追溯链条 - 任何函数的
__proto__
都指向Function.prototype
,Foo.__proto__=== Function.prototype
- 所有
__proto__
最终都会指向Object.prototype
,然后指向null
,Object.prototype.__proto__ === null
_ _ Function
比较特殊,他属于自身的实例(constructor
也是自身),所以Function.__proto__=== Function.prototype
Object
也比较特殊,它属于Function
的实例,所以Object.__proto__===Function.prototype
。
new关键字做了什么
new
关键字会返回一个新的实例,同时会给新对象构造原型链:
- 创建一个空对象,并使该空对象继承
Function.prototype
(新对象的__ proto__
等于 构造函数的prototype
) - 执行构造函数,并将
this
指向刚刚创建的新对象; - 返回新对象;
其原理为:
function _new (){
//1取出函数名,并且将函数名从参数中删除
var Func = [].shift.call(arguments)
//2.1构造一个空对象
var obj = {};
//2.2空对象继承Func.prototype 【2.1 2.2可用 Object.create(Func.prototype)代替 (Object.create使用现有的对象来提供新创建的对象的__proto__)】
obj.__proto__ = Func.prototype;
//3. 执行构造函数
var reuslt = Func.apply(obj,arguments)
// 4. 返回值:如果无返回值或者返回一个非对象值,则将新对象返回;如果返回值是一个新对象的话那么直接返回该对象。
if (result !== null && typeof result === "object") {
return result;
} else {
return obj;
}
}
对象封装
为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。
构造函数(Constructor)
构造函数其实也是普通的函数,只不过内部使用了 this
变量,对构造函数使用 new
运算符,就能生成实例,且 this
变量会绑定在新的实例上。
function Dog (name) {
this.a = 1
this.name = name
this.bark = function(){console.log('wangwang')}
}
var d1 = new Dog('one') // new一实例
var d2 = new Dog('two') // new一实例
此时 f1
就是 Foo
的一个实例,同时 f1
会拥有一个 constructor
的属性指向 Foo
相关判断方法
d1.constructor === Dog //true
d1 instanceof Dog // true
缺点
缺点:公共部分的属性不能复用,造成资源浪费
d1.bark===d2.bark //false
构造函数+原型链 (prototype)
Javascript规定,每一个构造函数都有一个prototype
属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
且构造函数的 constructor
属性就是该构造函数。
function Dog (name) {
this.name = name
}
Dog.prototype.bark = function(){console.log('wangwang')}
Dog.prototype.constructor === Dog //true 构造函数的 constructor 属性就是该构造函数
var d1 = new Dog('one') // new一实例
var d2 = new Dog('two') // new一实例
d1.bark===d2.bark //true
相关判断方法
isPrototypeOf
判断函数的prototype和实例的关系hasOwnProperty
判断实例的方法是自身的属性还是从prototype继承而来in
遍历实例的所有属性(自身+继承)
Dog.prototype.isPrototypeOf(d1) //true
d1.hasOwnProperty('name') //true
d1.hasOwnProperty('bark') //false
for(let p in d1){
console.log(p) //line1:name line2:bark
}
对象继承
继承是开发中比较常见的开发方法,JavaScript中又有哪些继承方式呢?
构造函数继承(apply或者call)
仅继承父类自身的属性和方法。
通过这种方式继承的对象不包含父类的原型链上的属性,因为整个过程中并没有调用 new 产生实例,相当子类将父类的独享属性拷贝了一份到自己内存中。其后对子类做任何修改都不会影响到父类。
核心:在子类构造函数的内部调用超累构造函数,继承父类的属性和方法
场景:子类仅需要独享父类的自身属性
缺点:
- 每个实例都会单独拥有一份不能共用的方法和变量,违背了代码复用的原则。
- 无法继承父类的原型。(父类的原型链上的属性,对子类型而言也是不可见的)
function Animal(){
this.type = 'animal'
}
function Dog (name) {
Animal.apply(this,arguments) //重点
this.name = name
}
var d1 = new Dog('one')
var d2 = new Dog('two')
d1.type //animal
d2.type //animal
原型继承(prototype)
继承父类自身的属性及原型链上的属性和方法。
核心:子类构造函数的prototype指向父类的一个实例,继承原型链上的属性和方法
场景:子类可以与其他子类共享父类自身和__proto__上的属性
缺点:
- 引用类型值的误修改:原型属性中的引用类型属性会被所有实例共享,子类实例更改从父类原型继承来的引用类型共有属性会影响其他子类。
- 无法传递参数:由于子类的继承是靠其prototype对父类的实例化实现的,所以无法传递参数。
function Animal(){
this.type = 'animal'
this.other = {}
}
function Dog (name) {
this.name = name
}
Dog.prototype = new Animal() // Dog.prototype.__proto__ === Animal.prototype ,同时这里会使得 Dog.prototype.constructor === Animal
Dog.prototype.constructor = Dog
var d1 = new Dog('one')
var d2 = new Dog('two')
d1.type //'animal'
d2.type //'animal'
d1.other.a = 1 // 修改d1的引用属性
d2.other.a // 1 d2的也被修改
组合继承 (构造函数继承+原型继承)
使用构造函数,继承父类自身的属性和方法,使得每个子类实例的属性和方法隔离;使用原型继承,继承父类prototype上的属性和方法,共用这些属性和方法。
核心:构造函数继承父类自身的属性和方法+原型继承原型链上的属性和方法
场景:子类实例从父类继承的属性隔离,且可以使用父类原型链上的属性
缺点:
- 父类构造函数的重复调用执行,会造成浪费,如果父类构造函数共有属性极多,会导致运行速度减慢。
function Animal(){
this.type = 'animal'
this.other = {}
}
Animal.prototype.say = function(str){
console.log('saying...',str)
}
function Dog (name) {
Animal.call(this,arguments)
this.name = name
}
Dog.prototype = new Animal() //疑问 可以 Dog.prototype = Animal.prototype ,但是对Dog.prototype的修改也会影响Animal!!!
Dog.prototype.constructor = Dog
// 实例测试
var d1 = new Dog('one')
var d2 = new Dog('two')
d1.say===d2.say // true
d1.other.a = 1 // 修改d1的引用属性
d2.other.a // undefined
寄生式继承(Object.create)
与原型链继承思想类似,使用 Object.create
或者是其他可以返回克隆对象的函数,对现有的对象实例继承其属性。
核心:**Object.create**
** 返回新对象**
场景:需要继承一个实例且只有部分方法不一样
缺点:
- 由于
Object.create
的核心是a.__proto__=cloneFun.prototype
,所以会跟原型链继承一样,有引用类型值的误修改问题
区别:
- 与原型继承类似,但是在其基础上添加了在创建实例的函数中以某种形式来增强对象,最后返回对象。
function Dog (name) {
this.name = name
this.other = {}
this.say = function(){
console.log('wangwang')
}
}
var d1 = new Dog('one')
var specialDog = Object.create(d1);
specialDog.__proto__ === d1 // true
specialDog.say = function(){
console.log('special: wangwang')
}
// 实例测试
d1.say() // wangwang
specialDog.say() //special: wangwang
specialDog.other.a = 1 // 修改specialDog的引用属性
d1.other.a // 1 d1的也被修改
ps:
//Object.create一个参数时相当于
function _Object(o){
function A(){}
A.prototype = o //传入对象o作为临时构造函数的原型对象
A.prototype.constructor = A //修改构造函数指向
return new A() //返回新的对象 这个新对象的 .__proto__ === A.prototype === o
}
组合寄生继承
使用构造函数,继承父类自身的属性和方法,使得每个子类实例的属性和方法隔离;使用寄生的形式增强子类的原型对象,继承父类prototype上的属性和方法(避免了组合集成里面父类构造函数执行两次的缺陷),共用这些属性。
核心:构造函数继承父类自身的属性及方法+寄生继承父类原型链上的属性和方法
场景:子类实例从父类继承的属性和方法隔离,且可以使用父类原型链上的属性和方法
缺点:
- …
function parasitic(subType,superType){
var _prototype = Object.create(superType.prototype) //浅拷贝父类的prototype 相当于_prototype.__proto__ = superType.prototype
_prototype.constructor = subType//修改构造函数指向
subType.prototype = _prototype //修改子类的原型
}
function Animal(){
this.type = 'animal'
this.other = {}
}
Animal.prototype.say = function(str){
console.log('saying...',str)
}
function Dog (name) {
Animal.call(this,arguments)
this.name = name
}
parasitic(Dog, Animal)
//类测试
Dog.__proto__ === Animal // false Dog是一个函数,所以他的原型是Function.prototype
Dog.prototype.__proto__ === Animal.prototype //true
// 实例测试
var d1 = new Dog('one')
var d2 = new Dog('two')
d1.say===d2.say // true
d1.other.a = 1 // 修改d1的引用属性
d2.other.a // undefined
ES6 class extend继承
es6提供了extend关键字的继承。
核心:super函数继承父类的属性和方法+子类直接继承父类的原型链上的属性和方法
场景:子类实例从父类继承的属性隔离,且可以使用父类原型链上的属性
- 继承的class的原型(
Dog.__proto__
)就是extends
关键字后的父类,Dog.__proto__ === Animal
(这点与模拟的继承有很大的不同) - 继承的class的实例的公共属性(
Dog.prototype
),其原型为extends
关键字后的父类的实例的公共属性(Animal.prototype
)
class Animal{
constructor(){
this.type = 'animal'
this.other = {}
}
}
class Dog extends Animal{
constructor(props){
super(props)
this.name = props.name
}
say(){
console.log('wangwang')
}
}
//类测试
Dog.__proto__ === Animal // true 继承的原型就是Animal, 这跟我们模拟的继承不一样
Dog.prototype.__proto__ === Animal.prototype // true,Dog函数的原型对象的相当于Animal的原型属性 与我们模拟的继承一样
//实例测试
var d1 = new Dog('one')
var d2 = new Dog('two')
d1.say===d2.say // true
d1.other.a = 1 // 修改d1的引用属性
d2.other.a // undefined
结论
- 最好的父类属性继承方式:构造函数继承
- 最好的原型继承方式:寄生继承(注意是对原型对象进行增强)
- 最好的继承方式:class extend 继承(正确的废话,这是ES的标准= =)
- 组合寄生继承和class extend 继承的不同
- 继承的核心思路不同
- 继承后的子类的原型指向不同,
class
直接指向父类,而函数的均指向Function