JS继承(原型、原型链)
这里写目录标题
要了解JS继承,首先要明白构造函数,实例和原型的关系。简单来说原型链的产生就是一个实例的原型又是由另一个构造函数构造出来的实例对象(即另一个类型的实例)
关于原型
定义
原型是function对象的一个属性,它定义了构造函数制造出的对象的公共祖先(注意是公共祖先,而不是构造函数中的内容)。通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象。
(原型上的方法,构造函数构造出来的对象都能使用)
//Person函数是对象
//Person.prototype -- 原型(在被构造函数定义时就产生了)
//Person.prototype = {} 是祖先(它也是一个对象,里面有定义的属性和方法)
Person.prototype.LastName = "Wei";//原型的一个属性
Person.prototype.say = function(){
console.log("hehe");
}//原型的一个方法
function Person(){
//Person中LastName和say,但是继承了祖先的属性和方法
}
var person = new Person();
var person1 = new Person();
//person 和 person1 都继承了该原型的属性和方法
console.log(person.LastName);//得到"Wei"
function Person(name,age){
this.name = name;
this.age = age;
}
var person = new Person("junjie",19);
//一个正常的对象有自己的属性和方法也有继承而来的属性和方法
//原型的一般用法:转移构造函数中一些固定的属性和方法到原型中,减小耦合
隐式属性 __proto__
- 一个对象如何查看他的原型
Car.prototype = {
LastName : "WEI",
}
function Car(name){
this.name = name;
}
var car = new Car("junjie");
console.log(car.__proto__);
//结果是得到原型,点开里面有constructor:f Car(name)和__proto__
//可以通过命明规则来说明自己私人的属性,比如 var _pravate=~;
//过程:在用new构造对象的吗,时候,发生下面过程
//在Car 里面 var this = { __proto__ : Person.prototype};
//这里的作用相当于连接原型和构造函数
//每一个新产生的car中都有__proto__属性来放原型
//在查找属性LastName时,car会先在自己的属性中查找,然后顺着__proto__再在Car.prototype中查找
//同样可以用于修改, car.__proto__ = obj{LastName : "Wang"}
注意
Person.prototype.name = "sunny";
function Person(){
//var this = {__proto__:Person.prototype}
}
var person = new Person();
Person.prototype = {
name : "cherry"
}
console.log(person.name);
//注意这里得到的结果是"sunny"而不是"cherry"
//因为在声明新对象person的时候,它的__proto__连接的是原来的Person.prototype
/* 过程理解大致如下:
首先是预编译,提升变量person和function Person
开始执行。先给Person的原型增加新属性name : "sunny"
然后person = new Person(),person变成一个对象
person.__proto__里面的存的是 obj={name:"sunny",constructor:Person()}
下一步执行Person.prototype={name:"cherry"}
因为person的proto属性早就被定义好了
所以这里的修改该的是原型的属性,而不是person中的__proto__,
person在自身属性找不到name时,会在__proto中查找;
简化流程:
Person.prototype = {name : "sunny"}
person.__proto__ = Person.prototype;
Person.prototype = {name : "cherry"}
*/
//注意顺序会影响结果,如果将6~8行代码,放在peoson = new Person()之前
//结果就是"cheery"
constructor
- 独立生成对象如何查看对象的构造函数
function Car(){
}
var car = new Car();
console.log(car.constructor);
//这里的结果会显示构造函数,说明原型中有这个constructor属性,这是系统自动加的
/*
原型中Car.prototype = {
constructor:Car(),
_proto_:Object
}
*/
//这时候可以改变constructor指向
//一种方式:Car.prototype.constructor = Person;
//另一种方式:Car.prototype = {constructor : Person}
console.log(car.constructor);
//得到的是 Person(){}
关于原型链
样例
//原型的__proto__属性,Grand.prototype.__proro__等于Object.prototype
//Object.prototype是所有对象的最终原型
//访问Object.prototype.__proto__得到null,说明这是终端
Grand.prototype.lastName = "Deng";
function Grand(){
this.name = "daming";
}
var grand = new Grand();
grand.name = "dadaming"
Father.prototype = grand;//对象中的属性赋给了Father的原型
//对象中含有name : "dadaming"和__proto__:Grand.prototype
function Father(){
this.name = "xiaoming";
}
var father = new Father();
//新对象father中的属性有 name:"xiaoming",__proto__:Father.prototype
function Son(){
this.name = "xiaoxiaoming";
}
Son.prototype = father;//father中的属性赋给了Son的原型
//father中有 name:"xiaoming",__proto__:Father.prototype
var son = new Son();
//这时候son里面有的属性是:name:"xiaoxiaoming",
//__proto__:Son.prototype
console.log(son.lastName);
/*这里通过原型链访问,son-->son.__proto__(即father)
-->father.__proto__(即grand)-->grand.__proto__,lastName
有点类似作用域链
原型链上属性的增删改查
-
一般子不能修改父,因为只会在自身上面增加新属性
//沿用上例 function Father(){ this.name = "xiaoming"; fortune = { card1 : "visa", card2 : "master" } } //访问son.fortune可以看到父元素的fortune对象 son.fortune = {card1:"mi",card2:"huawei"}; console.log(father.fortune);//这里father中的fortune对象没改 //变的是son中增加了对象fortune,里面含两个属性 //但是以下方式可修改(忽略第10 11行代码) son.fortune.card2 = "mi"; //因为fortune后面跟属性,可以说forture是引用值 //这里相当于son访问的是引用值fortune的card2这个原始值 //沿用上例 function Father(){ this.num = 100; } son.num++; //结果是son中增加了一个新属性num 值是101,而father中没有变
-
this的归属问题
Person.prototype = { name : "a", sayName : function(){ console.log(this.name); } } function Person(){ name : "b"; } var p1 = new Person; p1.name = "c"; console.log(p1.sayName()); //这里的检索过程应该是 //p1-->p1.__proto__(即Person.prototype) //找到函数体后执行,this.name //这时候的this指的是p1,因为是p1过来调用的 //所以结果是"c",如果没有第12行代码,结果就是"b" //因为name = "b" 是所有新造对象都共有的
Object.prototype
- 绝大多数对象的最终都会继承自身Object.prototype
var obj1={};
//这是一个自变量对象的创建
//其实质是调用了系统函数即 var ovj1 = new Object();
//但一般都不用调用系统函数的写法。
//obj1.__proto__ ----> Object.prototype
//绝大多数对象的最终都会继承自身Object.prototype
//例外(联系之前的Object.create())
call/apply
作用
改变this的指向
function Person(name,age){
this.name = name;
this.age = age;
}
var person = new Person("deng",100);
var obj = {}
//一般来说执行一个函数test()相当于teat.call(),call是一个方法
Person.call(obj,"cheng",300);
//这里让Person中所有的this都变成obj,同时传入参数
/*得到
obj = {name:"cheng",age=300}
//实质:利用call来给obj按Person工厂来制作属性
实现过程(借用别人函数实现自己功能)
function Person(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
}
function Student(name,age,sex,tel,grade){
this.name = name;
this.age = age;
this.sex = sex;
this.tel = tel;
this.grade = grade;
}
var student = new Student("wei",19,"male",137,2020);
/* 用法:
将第7~9行变为 Person.call(student,name,age,sex);
但是上面这个具有唯一性,只针对student
所以要改为 Person.call(this,name,age,sex);
*/
function Student(name,age,sex,tel,grade){
Person.call(this,name,age,sex);
this.tel = tel;
this.grade = grade;
}
apply和call区别不大
区别
后面传的参数形式不同 。
call 需要把实参按照形参的个数传进去
apply 只能传一个参数,是数组
Person.apply(this,[name,age,sex]);
总结
两者功能相同,都是改变this指向,但传参列表不同。
继承
原型模式共享
- 存在的问题:
- 对于引用类型的属性,会共享使用同一份数据
- 缺少参数传递的过程
Father.prototype.lastName = "Deng";
function Father(){
}
function Son(){
}
Son.prototype = Father.prototype;
//Son.prototype = new Father();类似
var son = new Son();
var father = new Father();
//封装一个函数实现继承
//继承的两中语法
//function extend(){}
function inherit(Target,Origin){
Target.prototype=Origin.prototype;
}
//表示Target继承至Origin,传进来的应该是构造函数
/*
这里要注意顺序的问题:
要先执行封装函数,再赋值son= new Son();
否则不生效
*/
借用构造函数
- 又称"对象伪装"或"经典继承"
function SuperType(name){
this.colors = ["red","blue","green"];
this.name = name
}
function SubType(){
// 继承SuperType
// 在SubType实例出的对象上执行SuperType的初始化代码
SuperType.call(this,"Joseph");
}
var app1 = new SubType();
app1.colors.push("black");
console.log(app1.colors);// ["red","blue","green","black"]
var app2 = new SubType();
console.log(app2.colors);// ["red","blue","green"]
//每个实例中的colors是属于自己的
- 但是app1的原型链上是不存在SuperType.prototype的,所以我们实例出来的对象是不能使用父类原型上定义的方法的
- 相比原型模式的共享
- 利用借用构造函数的方法我们可以解决原型共享中的共享数据问题
- 优点是可以由子类构造函数中向父类构造函数中传递参数
- 因为必须在构造函数中定义方法,因此函数不能重用
总言之,上面两者基本上也不能单独使用的
组合继承(结合上面两者)
- 又称伪经典继承(是Javascript中使用最多的继承模式)
- 并且还保留了
instanceof
操作符和isPrototypeOf()
方法
function Father(name){
this.colors=["red","yellow","blue"];
this.name = name
}
Father.protoType.sayName = function(){
console.log(this.name);
}
function Son(name,age){
this.age = age;
Father.call(this,name);
}
Son.protoType = new Father();
var son1 = new Son("Joseph",19);
var son2 = new Son("Koseph",21);
instanceof和isPrototypeOf()
Father.prototype.isPrototypeOf(son1);//true
son1 instanceof Father;//true
原型式继承
- 首先是你有一个对象,想在它的基础上再创建一个新对象。你只需要把这个对象先传给object(),然后再对返回的对象进行适当修改。
//实现的函数
function object(o){
function F(){};
F.prototype = o;
return new F();
}
使用样例
function object(o){
function F(){};
F.prototype = o;
return new F();
}
let person = {
name:'Nicholas',
friends:["Shelby","Court","Van"]
sayName: function(){
console.log(this.name);
}
};
let person1 = object(person);
person1.name = "Joseph";
person1.friends.push("LJJ");
let person2 = object(person);
person2.name = "Koseph";
person2.friends.push("ZJL");
console.log(person2.friends);//["Shelby","Court","Van","LJJ","ZJL"]
console.log(person1.name);//"Joseph"
console.log(person.name);//"Nicholas"
ES5中的Object.create()
-
如果只接收一个参数,它的作用就与上述的object相同。
-
第二个参数可选择性接收,接收的是给新对象定义额外属性的对象。第二个参数Object.defineProperties()上的第二个参数一样,每个新增的属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性
let person = { name:'Nicholas', friends:["Shelby","Court","Van"] sayName: function(){ console.log(this.name); } }; let p1 = { name: { //value是描述符,如果直接name:"Joseph会报错" value: "Joseph", }, sayHi: function () { console.log("Hi"); }, }; let person1 = Object.create(person, p1); person1.friends.push("LJJ"); let person2 = Object.create(person); person2.friends.push("ZJL"); console.log(person2.friends);//["Shelby","Court","Van","LJJ","ZJL"] console.log(person1.name); //"Joseph" console.log(person.name); //"Nicholas"
总结:原型式继承非常适合不需要单独创建构造函数,但仍需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会共享,跟使用原型模式是一样的
寄生式继承
- 与原型式继承比较接近。创造一个实现继承的函数,以某种方式增强对象,然后返回这个对象
function createAnother(original){
let clone = object(original); //通过调用函数来创建一个新对象
clone.sayHi = function(){ //以某种方式增强这个对象
console.log("Hi");
}
return clone; //返回对象
}
let person = {
name:'Nicholas',
friends:["Shelby","Court","Van"]
sayName: function(){
console.log(this.name);
}
};
let p1 = createAnother(person);
p1.sayHi();
- 新返回的p1对象具有person的属性和方法,还有一个新方法叫sayHi
- 注意:通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似
寄生式组合继承
- 组合继承其实也会存在效率问题–父类构造函数始终会被调用两次
function Father(name){
this.colors=["red","yellow","blue"];
this.name = name
}
Father.prototype.sayName = function(){
console.log(this.name);
}
function Son(name,age){
this.age = age;
Father.call(this,name);
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
//这里是第一次调用SuperType
//其实这个时候Son的原型上已经有了colors和undefined的name
//这两个可以说是Father的实例属性,但现在成为了Son的原型属性
//也就是说在调用Son构造函数时,也会调用Father构造函数
- 注意:新对象上创建实例属性name和colors来遮蔽原型上的同名属性(即colors和undefined的name),所以说调用了两次Father构造函数
解决
- 基本思路:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。其实就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。如下
function inheritPrototype(subType,superType){
let prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
//首先是创建父类原型的一个副本。然后返回的prototype对象设置constructor属性,解决由于重写原型导致默认constructor丢失的问题。最后将新创建的对象赋值给子类型的原型
样例
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function(){
console.log(this.age);
}
- 寄生式组合继承可以算是应用类型继承的最佳模式
- 优点
- 只调用了一次SuperType构造函数
- 避免了SubType.prototype上不必要也用不到的属性,所以可以说这个例子效率更高
- 原型链仍然保持不变,instanceof操作符和isPrototype()方法正常有效
参考文献:《JavaScript高级程序设计(第四版)》