类
构造函数是由new关键字决定的,任何函数都可以是构造函数,但是没有使用new关键字的函数是普通函数,只有使用new关键字调用的函数才是构造函数
在访问一个对象身上的属性或者函数时首先会在对象自身查找是否有这个属性(函数),如果没有就继续递归往它的prototype上去找,找到的话就调用,直到protytype为空时还没有找到就会报错
JavaScript
常被描述为一种基于原型的语言——每个对象拥有一个原型对象 当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾 准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype
属性上,而非实例对象本身
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法 在对象实例和它的构造器之间建立一个链接(它是__proto__
属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法
- 一切对象都是继承自
Object
对象,Object
对象直接继承根源对象null
- 一切的函数对象(包括
Object
对象),都是继承自Function
对象 Object
对象直接继承自Function
对象Function
对象的__proto__
会指向自己的原型对象,最终还是继承自Object
对象
prototype
是构造函数上的属性 __prot__
是对象上的属性,它指向自己构造函数的prototype也叫隐式原型
8. 继承
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”
继承的优点
继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码
在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能
实现方式
原型链继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针
/**
* 原型链继承
* 缺点:两个实例使用的是同一个原型对象,内存空间是共享的,在一个对象上修改父类原型上的数据,其他继承同一个父类的对象也会被改变
* @param {*} Child 子类构造函数
* @param {*} Parent 父类构造函数
*/
function myExtends1(Child, Parent) {
Child.prototype = new Parent();
Child.prototype.constructor = Child;
}
// 测试原型链继承
function test1() {
function Parent() {
this.colors = ["red", "blue"];
}
Parent.prototype.say = function () {
console.log("Parent method");
};
function Child() {}
myExtends1(Child, Parent);
const c1 = new Child();
const c2 = new Child();
c1.colors.push("green");
console.log("原型链继承:");
console.log(c1.colors); // ["red","blue","green"]
console.log(c2.colors); // ["red","blue","green"] <-- 引用属性共享
c1.say(); // 可继承父类原型方法
}
改变c1
的属性,会发现c2
也跟着发生变化了
缺点 两个实例使用的是同一个原型对象,内存空间是共享的,在一个对象上修改父类原型上的数据,其他继承同一个父类的对象也会被改变
构造函数继承(借助 call)
通过在子类中调用父类的构造函数实现子类身上有父类的属性(函数)
借助 call
调用Parent
函数
/**
* 构造函数继承
* 缺点:无法继承原型上的属性和方法
* @param {*} Child 子类构造函数
* @param {*} Parent 父类构造函数
*/
function myExtends2(Child, Parent) {
return function (...args) {
Parent.call(this, ...args);
Child.apply(this, args);
};
}
// 测试构造函数继承
function test2() {
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.say = function () {
console.log("Parent method");
};
function Child(name, age) {
this.age = age;
}
const NewChild = myExtends2(Child, Parent);
const c1 = new NewChild("Tom", 18);
const c2 = new NewChild("Jerry", 20);
c1.colors.push("green");
console.log("构造函数继承:");
console.log(c1.name, c1.age, c1.colors); // Tom 18 ["red","blue","green"]
console.log(c2.name, c2.age, c2.colors); // Jerry 20 ["red","blue"]
console.log(c1.say); // undefined <-- 缺点:原型方法继承不到
}
父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法 相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承父类原型(prototype)属性或者方法
缺点 如果在父类的prototype上定义了属性和方法使用构造函数继承的方式子类无法继承父类的prototype上的属性和方法
组合继承
前面两种继承方式,各有优缺点。组合继承则将前两种方式继承起来
/**
* 组合继承
* 缺点:父类构造函数执行了两次
* @param {*} Child 子类构造函数
* @param {*} Parent 父类构造函数
*/
function myExtends3(Child, Parent) {
function TemplateChild(...args) {
Parent.call(this, ...args);
Child.apply(this, args);
}
TemplateChild.prototype = new Parent();
TemplateChild.prototype.constructor = Child;
return TemplateChild;
}
// 测试组合继承
function test3() {
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.say = function () {
console.log(this.name);
};
function Child(name, age) {
this.age = age;
}
const NewChild = myExtends3(Child, Parent);
const c1 = new NewChild("Tom", 18);
const c2 = new NewChild("Jerry", 20);
c1.colors.push("green");
console.log("组合继承:");
console.log(c1.colors, c2.colors); // 独立属性
c1.say(); // Tom
// 缺点:Parent 构造函数被调用两次
}
这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent()
执行了两次
缺点 父类的构造执行了两次,造成了多构造一次的性能开销
原型式继承
这里主要借助Object.create
方法实现普通对象的继承
/**
* 原型式继承
* 缺点:因为是浅拷贝原型对象属性是共享的,多个实例会引用同一个原型对象
* @param {*} obj 原型对象
* @returns 新对象
*/
function myExtends4(obj) {
return Object.create(obj);
}
// 测试原型式继承
function test4() {
const parent = {
name: "Parent",
colors: ["red", "blue"],
};
const child1 = myExtends4(parent);
const child2 = myExtends4(parent);
child1.colors.push("green");
console.log("原型式继承:");
console.log(child1.colors, child2.colors); // 共享引用属性
}
这种继承方式的缺点也很明显,因为Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能
缺点 由于create对于引用类型的属性采用的是浅拷贝(基本数据类型直接拷贝无影响),所以在修改一个对象的引用类型的数据时和它一起继承同一个父类的其他对象的数据也会被修改
寄生式继承
寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法
/**
* 寄生式继承
* 缺点:因为是浅拷贝原型对象属性是共享的,多个实例会引用同一个原型对象
* @param {*} obj 原型对象
* @returns 新对象
*/
function myExtends5(obj) {
const clone = Object.create(obj);
// 增加自己需要的方法
clone.say = function () {
console.log("Say");
};
return clone;
}
// 测试寄生式继承
function test5() {
const parent = { name: "Parent", colors: ["red", "blue"] };
const child = myExtends5(parent);
child.say(); // Say
child.colors.push("green");
console.log(parent.colors); // 引用属性依旧共享
}
其优缺点也很明显,跟上面讲的原型式继承一样
寄生组合式继承
寄生组合式继承,借助解决普通对象的继承问题的Object.create
方法,在几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式
/**
* 寄生组合式继承
* 缺点:因为是浅拷贝原型对象属性是共享的,多个实例会引用同一个原型对象
* @param {*} Child 子类构造函数
* @param {*} Parent 父类构造函数
*/
function myExtends6(Child, Parent) {
function TemplateChild(...args) {
Parent.call(this, ...args);
Child.apply(this, args);
}
TemplateChild.prototype = Object.create(Parent.prototype);
TemplateChild.prototype.constructor = Child;
return TemplateChild;
}
// 测试寄生组合继承
function test6() {
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.say = function () {
console.log(this.name);
};
function Child(name, age) {
this.age = age;
}
const NewChild = myExtends6(Child, Parent);
const c1 = new NewChild("Tom", 18);
const c2 = new NewChild("Jerry", 20);
c1.colors.push("green");
console.log("寄生组合继承:");
console.log(c1.colors, c2.colors); // 独立属性
c1.say(); // Tom
}
可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题
ES6
中的extends
关键字实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式
缺点 缺点:子类中的prototype的原始属性和方法会丢失。
new关键字
执行一个构造函数、返回一个实例对象,根据构造函数的情况,来确定是否可以接受参数的传递
所做的流程
- 创建一个新对象
- 将构造函数的作用域赋给新对象(this指向新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
new和直接调用构造函数的区别 使用new关键字调用构造函数返回的是一个构造的新对象 直接使用构造函数返回值与构造函数的返回值有关
贡献者
版权所有
版权归属:wynnsimon