1 构造函数和原型
1.1 构造函数
在典型的OOP语言中,都存在着类的概念,类是对象的模板,对象是类的实例。
ES6之前,JS没有类的概念,对象不是基于类创建的,而是用构造函数来定义对象的属性和方法。 构造函数创建对象的过程: juejin.cn/post/700815…
1.2 构造函数存在的问题:
函数是复杂数据类型,会在内存中再开辟一个额外空间存放函数,存在浪费内存的问题。
而,构造函数通过原型分配的函数是所有对象所共享的。 不会浪费内存
一般情况下,公共属性定义到构造函数里,公共方法放到原型对象里。
2 原型及原型链
什么是原型?对原型链是如何理解?原型及原型链所有的知识点都来自于以下理论:
原型是什么:一个对象,也称为prototype 或 原型对象。
原型的作用:共享方法。把那些不变的方法直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。
- 每个函数都有 prototype 属性。
- 每个对象都有 proto 属性(这个属性称之为原型)。 执行 new 的时候,对象的 proto 指向这个构造函数的 prototype 原型对象。
2.1 每个函数都有一个 prototype 属性
每个函数都有个 prototype 属性,这个属性指向函数的原型对象,同时 prototype 里面有个 constructor 属性回指到该函数:
function Demo() {} Demo.prototype.constructor === Demo; // true
- Demo.prototype 结构如下所示:
2.2 原型链
1️⃣✅:当使用 new 操作符后,Demo 就变成了构造函数。🔽
-
//使用 new 操作符来创建一个实例对象: function Demo() {} const d = new Demo(); // d 就是创建出来的实例对象
2️⃣✅:d 既然是对象,自然有 proto(原型),
对象原型__proto__
和原型对象prototype
是等价的,对象原型__proto__
指向构造函数 Demo 的 prototype:🔽
-
function Demo() {} const d = new Demo(); d.__proto__ === Demo.prototype; // true
3️⃣✅:
__proto__
对象原型的意义在于为对象的查找机制提供一个方向,它只是内部指向原型对象prototype,是一个非标准属性,实际开发中不能用。🔽
- 当访问一个对象的属性时,程序会先去这个对象里面查找,如果没有找到会去这个对象的原型上查找,如下所示:
function Demo() { this.name = "Demo"; } Demo.prototype.say = function () { console.log("我是", this.name); }; const d = new Demo(); // 虽然 Demo 上没有 say 方法,但是因为Demo的prototype上有此方法,所以下面的调用可以正常打印。 d.say(); // 我是Demo
- 可以用一张图来描述这个查找过程:
4️⃣✅:这里只体现出一层查找,实际上 proto 是逐层向上查找的,这个查找过程也就是我们所说的原型链。🔼
3 继承
JavaScript 如何实现继承?,在 ES6 之前,都是通过原型的方式来实现,这也是原型的重要使用场景之一,通常有如下几种实现方式:
3.1 原型赋值
//父类
function Parent(name) {
this.name = name||"Tina";
this.age = 20;
this.arr = [1,2,3]
}
Parent.prototype.say = function () {
console.log(this.name);
};
//子类
function Children() {}
Children.prototype = new Parent(); //直接new 了一个父类的实例,然后赋给子类的原型
Children.prototype.love = function () {
console.log(this.name);
};
//孩子1
const child1 = new Children("niki");//❌创建子类实例时,无法向父类构造函数传参。
console.log(child1.name); // Tina
child1.love();// Tina
child1.say();// Tina
//孩子2
const child2 = new Children();
console.log(child1.age,child2.age)//20 20
child1.age++
console.log(child1.age,child2.age)//20 21
console.log(child1.arr,child2.arr)//[ 1, 2, 3 ] [ 1, 2, 3 ]
child1.arr.push(4)
console.log(child1.arr,child2.arr)//[ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]
//❌原型属性若是引用类型,如Array,会被所有实例共享,所以对 child1.arr 的修改,child2.arr 也会被修改;基本类型没有这个问题。
- 这种方法:直接 new 了一个父类的实例,赋给子类的原型。相当于直接将父类原型中的方法属性以及挂在 this 上的各种方法属性全赋给了子类的原型。
- 简单粗暴,弊端很多,主要问题是:
- ❌创建子类实例时,无法向父类构造函数传参。
- ❌来自原型对象的所有属性和方法被所有实例共享,但无法做到属性、方法独享。
- 包含引用类型值的原型属性会被所有实例共享,某个子类实例修改父类的引用类型的值的话其他实例也会受到影响。(String、Number这种基本类型不被共享)。
- 方法也是这样的:Sub1修改了父类的函数,其他所有的子类Sub2、Sub3...想调用旧的函数就无法实现了。 这种方式不合适。
3.2 构造函数
//父
function Parent(name) {
this.name = name || "Tina";
}
Parent.prototype.say = function () {
console.log(this.name);
};
//子
function Children(name) {
Parent.apply(this, arguments); // 通过apply 调用 Parent 并改变 Parent 中的this指向
//在子的构造函数里面用apply改变this指向实现继承,还可以实现传参
}
const child1 = new Children("niki");//✅创建子类实例时,可以向父类构造函数传参。
console.log(child1.name); // niki
// child1.say(); // ❌error 不能继承父类原型的方法。
const child2 = new Children("Lili");
console.log(child1.arr,child2.arr)//[ 1, 2, 3 ] [ 1, 2, 3 ]
child1.arr.push(4)
console.log(child1.arr,child2.arr)//[ 1, 2, 3, 4 ] [ 1, 2, 3]
//✅某个子类实例修改父类的引用类型的值,其他实例不会受到影响。
- 这种方法:在子类的 prototype 通过改变 this 指向 来 调用父类,达到继承的目的。
- 缺点:
- 虽解决了传参的问题,但不能继承父类原型的方法,只能在函数体内定义。
- 没有复用可言了,没有继承原型。
这种方式也不合适。
注:apply
argument
3.3 组合继承
原型赋值
和构造函数
各有优缺点。
组合继承:
- 使用
原型赋值
来继承方法。 - 借用
构造函数
来继承属性。 从而发挥二者之长的一种继承模式。
//父
function Parent(name) {
this.name = name || "Tina";
this.arr = [1,2,3]
}
Parent.prototype.say = function () {
console.log(this.name);
};
//子
function Children(name) {
Parent.apply(this, arguments); //构造函数
}
Children.prototype = new Parent();//原型赋值
//Children.prototype.constructor = Children;//使得Children.prototype.constructor指向自己
const child1 = new Children("niki");//✅创建子类实例时,可以向父类构造函数传参。
child1.say(); // ✅ 能继承父类原型的方法。
const child2 = new Children("Lili");
child1.arr.push(4)
console.log(child1.arr,child2.arr)//[ 1, 2, 3, 4 ] [ 1, 2, 3]
//✅某个子类实例修改父类的引用类型的值,其他实例不会受到影响。
- 缺点:看似完美,却把父类的属性继承了两份:
- 如果父类本身比较大,这样实现继承对内存的消耗比较大,也不太合适。
3.4 寄生组合继承——完美的方式实现继承
//父
function Parent(name) {
this.name = name || "Tina";
this.arr = [1,2,3]
}
Parent.prototype.say = function () {
console.log(this.name);
};
//子
function Children(name) {
Parent.apply(this, arguments); //构造函数
}
Children.prototype = Object.create(Parent.prototype);//寄生继承
Children.prototype.constructor = Children;//使得Children.prototype.constructor指向自己
const child1 = new Children("niki");//✅创建子类实例时,可以向父类构造函数传参。
child1.say(); // ✅ 能继承父类原型的方法。
const child2 = new Children("Lili");
child1.arr.push(4)
console.log(child1.arr,child2.arr)//[ 1, 2, 3, 4 ] [ 1, 2, 3]
//✅某个子类实例修改父类的引用类型的值,其他实例不会受到影
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。- 寄生继承:
- 不需要实例化父类,直接实例化一个临时副本实现了相同的原型链继承。(子类的原型指向父类副本的实例从而实现原型共享,修改子类实例的属性不会影响到父类)。寄生组合继承使用寄生继承修复了组合继承的bug。
- 寄生继承与原型继承原理相同, 都是共用原型链,所以不可避免的会污染整个属性。
3.5 ES6 extends 实现继承
虽然说我们实现了继承,但是在 ES6 之前,实现一个继承还是相对复杂的,在 ES6 之后,我们有了 extends
关键字,很优雅的实现继承,如下所示:
class Parent {
constructor(name) {
this.name = name || "Tina";
}
say() {
console.log(this.name);
}
}
class Children extends Parent {
constructor(name) {
super(name); // 可以给父类传递参数
this.age = 16;
}
}
const child = new Children("lyh");
console.log(child); //{name: "lyh" , age: 16}
总结:JavaScript 是如何实现继承的?
继承方式 | 描述 | 优点 | 缺点 |
---|---|---|---|
原型继承 | 创建一个父类直接赋值给子类的 prototype | 引用值会被所有的实例共享; 不能给父类传参。 | |
构造函数继承 | 在子类中通过改变 this 指向来调用父类 | 解决了引用值共享的问题; 可以通过Child向Parent传参 | 不能继承父类的 prototype; 多执行了一次 apply() |
组合继承 | 结合原型继承和构造函数继承的优点 | 解决了引用值共享的问题; 可以通过Child向Parent传参; Parent上的原型可以被继承。 | 子类继承了两份父类,重复继承; 多执行了一次 apply() |
寄生组合继承 | 通过 Object.create 方式来优化组合继承 | 实现起来比较复杂 | |
ES6 extends 实现继承 | 可能在低版本浏览器存在兼容性问题。 |
参考
继承: www.cnblogs.com/l-y-c/p/134…
✨✨✨图解继承:www.cnblogs.com/libin-1/p/5…