简单讲讲js的继承,也是js的原型链问题的实际应用。markdown
原型和原型链都是来源于对象而服务于对象的概念:app
JavaScript中一切引用类型都是对象,对象就是属性的集合。函数
Array类型、Function类型、Object类型、Date类型、RegExp类型等都是引用类型。ui
每个对象从被建立开始就和另外一个对象关联,从另外一个对象上继承其属性,这个另外一个对象就是原型。this
当访问一个对象的属性时,先在对象的自己找,找不到就去对象的原型上找,若是仍是找不到,就去对象的原型的原型上找,如此继续,直到找到为止。若是在最顶层的原型对象也没有找到,就返回undefined
。 这条由对象及其原型组成的链就叫作原型链。spa
__proto__
属性虽然在 ECMAScript 6 语言规范中标准化,可是不推荐被使用,如今更推荐使用 Object.getPrototypeOf
,prototype
Object.getPrototypeOf(person) === person.__proto__
复制代码
function getProperty(obj, propName) {
if(obj.hasOwnProperty(propName)) {
return obj[propName]
} else if (obj.__proto__ !== null) {
return getProperty(obj.__proto__, propName)
} else {
return undefined
}
}
复制代码
从上图咱们能够看出code
__proto__
属性指向其原型对象,构造函数的prototype
属性指向其建立的对象实例的原型对象,因此对象的__proto__
属性等于建立它的构造函数的prototype
属性。原型链就是多个对象经过 __proto__
的方式链接了起来的一个链表结构。orm
Object
是全部对象的父节点,全部对象均可以经过 __proto__
找到它Function
是全部函数的父节点,全部函数均可以经过 __proto__
找到它__proto__
属性指向原型, __proto__
将对象和原型链接起来组成了原型链在理解对象继承以前得先弄明白建立对象这回事儿。对象
function createCar(color, passengers, brand){
var car = new Object();
car.color = color;
car.passengers = passengers;
car.brand = brand;
car.printBrand = function(){
console.log(this.brand)
}
return car;
}
const car = createCar('red', ['a','b'], 'benz')
复制代码
工厂模式很好理解,实例化一个对象,在把传入的参数放入该对象,再返回。
缺点:没法进行对象识别。因为返回的对象都是由Object对象实例化出来的,可是开发过程当中,须要建立不少种对象,确定会有进行对象识别的需求,工厂模式显然没法完成咱们这样的诉求。咱们继续探索。
function Car(color, passengers, brand){
this.color = color;
this.passengers = passengers;
this.brand = brand;
this.printBrand = function(){
console.log(this.brand)
}
}
const car1 = new Car('red', ['a','b'], 'benz');
const car2 = new Car('black', ['c','d'], 'BMW');
console.log(car1 instanceof Object); //true
console.log(car1 instanceof Car); //true
console.log(car2 instanceof Object); //true
console.log(car2 instanceof Car); //true
复制代码
从打印中能够看到 car1
与 car
的区别。
构造函数模式可以很好的使用 instanceof
进行对象的识别,Object
对象是全部对象的顶层对象类,全部的对象都会继承他。对对象进行操做的各种方法就存放在Object对象里面。function
实际上也是一个对象,从typeof
方法中能够体现出来
缺点:可是没法解决引用类型的建立问题,咱们每次对Car对象进行实例化的时候,都须要对printBrand方法进行建立,没法复用,浪费内存。要解决只能把他放到全局做用域。可是在全局做用域中定义的函数通常来讲只能被某个对象调用,这会让全局做用域名存实亡。而且也会失去封装性,咱们来想象一下,若是该对象中有不少方法,那会让全局做用域充满了单独拎出来的方法,让代码可读性变差。
function Car(){
}
car.prototype.color = "red";
car.prototype.passengers = ["a","b","c"];
car.prototype.brand = "benz";
car.prototype.printBrand = function () {
console.log(this.brand)
};
var car1 = new Car();
var car2 = new Car();
car1.color = "blue";
car1.passengers.push('d');
console.log(car1.brand); //["a","b","c","d"]
console.log(car2.brand); //["a","b","c","d"]
console.log(car1.color); // "bule"
console.log(car2.color); // "red"
复制代码
这个模式利用了对象的原型,将基本参数挂载在原型上面。
缺点:省去了初始化参数,这一点有好有坏。最大的问题是对引用类型值的共享,car1和car2实例在实例化之后还会与Car类存在关系。若是对其赋值基本类型值的话,会在实例化的对象当中建立,而且调用时会首先在实例化对象中寻找。而对引用类型值进行操做的时候,会直接在原型对象的引用类型值上进行操做,因此会在全部实例中共享。
function Car(color,brand){
this.color = color;
this.brand = brand;
this.passengers = ["a","b","c"];
}
Car.prototype = {
constructor: Car,
printBrand: function () {
console.log(this.brand)
}
}
var car1 = new Car("red",'benz');
var car2 = new Car("blue","BMW");
car1.color = "blue";
car1.passengers('d');
console.log(car1.brand); //["a","b","c"]
console.log(car2.brand); //["a","b","c","d"]
复制代码
利用原型自定义构造函数,每一个实例都会存在一份实例的副本,同时利用原型方法共享的特性,最大程度节省了内存,也提供了向构造函数中传递参数的功能。为最佳实践。
function OldCar(){
this.color = "red";
this.passengers = ['a','b','c']
}
OldCar.prototype.getOldColor = function(){
return this.color;
}
function NewCar(){
this.color = "blue";
}
NewCar.prototype = new OldCar();
var car = new NewCar();
var car2 = new OldCar();
console.log(car.getOldColor()); //"blue"
console.log(car.passengers) // [ 'a', 'b', 'c' ]
console.log(car2.getOldColor()); //"red"
复制代码
原型链继承通俗易懂,利用原型链将两个类串起来。
缺点
function OldCar(name = 'default name'){
this.passengers = ['a','b','c'];
this.name = name
}
function NewCar(name){
OldCar.call(this, name);
}
复制代码
基本思路就是在子类的构造函数的内部调用超类的构造函数。由于函数只是在特定的环境中执行代码的对象。借用构造函数的方式能够解决引用类型的问题。使用call()和apply()方法,在子类中调用超类。这样每一个实例都会有本身的引用类型的副本了。
缺点:和构造函数建立对象一致的问题,方法都得在构造函数中定义,致使函数没法复用,形成内存的浪费。
function OldCar(brand){
this.brand = brand;
this.passengers = ['a','b','c']
}
OldCar.prototype.getBrand = function(){
return this.brand;
}
function NewCar(name, color){
OldCar.call(this,name) //第一次调用
this.color = color;
}
NewCar.prototype = new OldCar(); //第二次调用
NewCar.prototype.constructor = NewCar; //加强
NewCar.prototype.getColor = function(){
return this.color;
}
复制代码
组合继承集借用构造函数方法和原型链继承二者之长,复用了方法,也解决了引用类型的问题。
缺点:须要调用两次超类的构造函数,第一次是OldCar.call(this,name)
,第二次是new OldCar()
。下一步咱们须要解决的是超类的两次调用问题。
function A(){
}
A.prototype.name = 'py';
A.prototype.age = 12;
<!--等价于-->
A.prototype = {
name: 'py',
age: 12
}
A.prototype.constructor = A
复制代码
上面的例子中,上半部分是最基本的对原型的赋值,而下班部分的对原型的赋值A的原型的构造函数会变成Object(先new Object而后再赋值参数),因此须要显式的去加强构造函数。
为了解决组合继承的痛点,出现了寄生组合继承。
function OldCar(brand){
this.brand = brand;
this.passengers = ['a','b','c']
}
OldCar.prototype.getBrand = function(){
return this.brand;
}
function NewCar(name,color){
OldCar.call(this,name)
this.color = color;
}
//继承开始
var middleObj = Object.create(OldCar.prototype);
middleObj.constructor = NewCar;
NewCar.prototype = middleObj
//继承结束
NewCar.prototype.getColor = function(){
return this.color;
}
复制代码
function createObj(obj){
function Car(){};
Car.prototype = obj;
return new Car();
}
Object.create() 等价于 crateObj(),至关于对传入的对象进行了一次浅复制。
复制代码
那么,咱们来看看继承的过程当中发生了什么。先对超类的原型进行一次浅复制。而后将中间对象的构造函数替换为普通类。为何要进行这一步?由于对超类的原型进行浅复制之后,中间对象的构造函数变成了Object,须要对该对象进行加强处理。最后将普通类的原型指向中间变量,这样就只须要调用一次超类就能够完成继承。