一、ES5中的类与继承
原型链继承
原型链是javascript的主要继承方式,主要思想是将父类的新实例复制给子类的原型。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SubType() {
this.subproperty = false;
}
//继承
SubType.prototype = new SuperType();
//将父类的实例赋值给子类的原型
实现继承的关键,在于subType没有使用默认原型,而是将其替换成了一个新的对象,该对象为superType的实例,不仅继承了属性和方法,还连接到了SuperType的prototype。
(1)判断实例与原型的关系
- instanceof(实例的方法)
如果一个实例的原型链上出现过相应的构造函数,那么返回true,
- isPrototypeof()(原型的方法)
只要原型链中包含这个原型,返回true
Object.isPrototypeOf(instance);
(2)原型链继承存在的问题
- 当原型中包含引用类型的变量时,一个实例对该属性修改会造成其他实例的该属性也会被修改
- 无法在不影响所有对象实例的情况下把参数传进父类的构造函数。
2. 盗用构造函数(对象伪装、经典继承)
通过在子类的构造函数中使用apply或者call方法调用父类的构造函数。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType() {
SuperType.call(this);
}
相当于复制父类的属性给子类,并没有用到原型。
优点:可以实现多继承,可以传递参数给父类型的构造函数。
缺点:必须在构造函数中定义方法,因此函数不能重用,子类也不能访问父类原型上定义的方法。
3.组合继承
综合了原型链和盗用构造函数**,基本思路是使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上实现重用,又可以让每个实例都有自己的属性。**
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType(name, age) {
//继承属性
//第二次调用
SuperType.call(this, name);
this.age = age;
}
//继承方法
//第一次调用父元素的构造函数
SubType.prototype = new SuperType();
组合继承是JavaScript中使用最多的继承模式。
缺点:调用了两次父类构造函数,
4. 原型式继承
使用object.create()函数,接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(可选)。
let person = {
name:"Nicholas",
friends: ["shelly", "sherlock"];
}
let anotherPerson = Object.create(person);
适用的情形:
- 有一个对象,想在其基础上新建一个对象,传入的对象将作为新对象的原型,也就是同时传入了属性值和方法。
- 非常适合不需要单独创建构造函数,但是仍然需要在对象间共享信息的场合。
- 注意⚠️:属性中的引用值始终会在相关对象中共享!
5.寄生继承
思路和工厂模式类似,调用一个仅用于封装过程的函数。
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function createAnother(original) {
//创建一个新对象
let clone = object(original);
//以某种方式增强这个对象
clone.sayHi = function () {
console.log("hi");
}
//返回这个对象
return clone;
}
以上方法接受一个参数,该参数为新对象的基准对象。
6.寄生式组合继承
主要是解决了组合继承的问题:调用了两次父类构造函数,使得相同的属性分别出现在了子类实例和子类原型上。
基本思路是不通过调用父类构造函数给子类原型赋值,而是使用寄生式继承来继承父类原型,将返回的新对象赋值给子类原型。使用盗用构造函数来继承父类实例的属性。
function inheritPrototype(subType, superType) {
let prototype = Object(superType.prototype);
prototype.constructor = subType;
subType.prototype =prototype;
}
接收两个参数:子类构造函数和父类构造函数
- 创建父类原型的一个副本,
- 给返回的prototype对象设置constructor属性,解决由于重写原型造成的constructor丢失问题。
- 将新创建的对象赋值给子类的原型。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType(name, age) {
//只调用了一次SuperType构造函数。避免了SubType.prototype上不必要也无用的属性。
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
二、ES6中的类
class类是ES6中引入的新的关键字,实际上使用的仍旧是原型和构造函数的概念。
1.类的定义
与函数类似,有两种方式:
//类声明
class Person{}
//类表达式
const Person = class{}
- 类定义不能提升。
- 函数受函数作用域限制,类受块作用域限制
2.类的组成
构造函数、实例方法等,默认情况下,类中定义的代码都在严格模式下执行。
(1)类构造函数
使用constructor关键字定义构造函数。告诉解释器在使用new创建类的新实例时, 应该调用这个函数。
使用new调用类的构造函数会执行如下操作:
- 在内存中创建一个新的对象
- 这个对象内部的prototype指针(即__proto__,隐式原)指向构造函数的prototype
- 构造函数内部的this被赋值给这个新对象
- 执行构造函数内部的代码,给新对象添加属性
- 如果构造函数返回非空对象那么返回(那么instanceof检测结果也可能和类无关),否则返回新创建的对象。
构造函数与类构造函数的区别:
- 类构造函数必须使用new操作符进行调用。
- 构造函数可以不使用new进行调用,就会以全局this(window)作为内部对象。
把类当作特殊函数
通过typeof进行检测,结果发现类就是function
类是JavaScript的一等公民,可以像其他对象或者函数引用一样把类作为参数传递,类可以像函数一样在任何地方定义。
类也可以向立即调用函数表达式一样,进行立即实例化。
let p = new class Foo{
constructor(x){
console.log(x);
}
}('bar');
3.实例、原型与类成员
(1)实例成员
在构造函数内部添加的,可以为新创建的实例添加**自有属性,**不会在原型上共享。
class Person {
constructor() {
//以下均为实例成员
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
}
(2)原型方法与访问器
类块中定义的方法作为原型方法,可以在实例之间进行共享。
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance');
}
locate(){ console.log('prototype'); }
}
虽然可以通过在类块中定义方法将其作为原型方法,但是不可以在类块中直接给原型添加原始值或者对象作为成员数据。 虽然不显示支持在原型或者类上添加数据,但是可以在类外部定义,手动添加:
class Person{
sayName(){
console.log(`${Person.greeting} ${this.name}`);
}
}
//在外部定义:
//1. 在类上定义成员:
Person.greeting = "Hello!"
//2. 在原型上定义数据成员
Person.prototype.name = 'jake';
(3)静态方法
静态方法通常用于执行不特定于实例的操作,使用static关键字作为前缀,在静态成员中,this引用自身(指类自身)。静态方法适合作为实例工厂。
lass Person {
constructor(age) {
this.age_ = age;
}
sayAge() {
console.log(this.age_);
}
static create() {
// 使用随机年龄创建并返回一个 Person 实例
return new Person(Math.floor(Math.random()*100));
}
}
4. ES6中的继承
ES6中使用extends关键字实现继承,子类可以通过原型链访问到类和原型上定义的方法,而this的值会反映调用相应方法的实例或者类。
(1)super
子类通过super关键字引用它们的原型,其中需要注意以下几点:
-
super关键字只能在子类中使用,且仅限于构造函数、实例方法和静态方法内部。
-
在类构造函数中使用super关键字可以调用父类的构造函数,并传递参数
class Vehicle { constructor() { this.hasEngine = true; } } class Bus extends Vehicle { constructor() { // 不要在调用super()之前引用this,否则会抛出ReferenceError super(); // 相当于super.constructor() console.log(this instanceof Vehicle); // true console.log(this); // Bus { hasEngine: true } } } -
在静态方法中可以通过
super调用继承的类上的静态方法:class Bus extends Vehicle{ static identity(){ super.identity(); } }
-
-
调用super()会调用父类的构造函数,并将返回的实例赋值给
this -
如果子类中没有定义构造函数,那么实例化子类实例时会调用
super(),而且会传入所有给派生类的参数。 -
在构造函数中使用super时,不能在调用super之前引用this
-
如果在子类中显示定义了构造函数,要么在构造函数中调用
super(),要么必须在其中返回一个对象。
5. 多继承(类混入)
ES6没有显式支持多类继承,但是可以通过现有特性可以轻松模拟这种行为。
可以通过定义一组可嵌套函数来实现,每个函数接收一个父类作为参数,将混入类定义为这个参数的子类,然后返回这个类:
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
} };
let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator,
}
current) => current(accumulator), BaseClass);
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
现在很多JS框架已经舍弃了混入模式,专项组合模式: 将方法提取到独立的类和辅助对象中,然后组合起来不使用继承。 也反映了设计原则:“composition over inheritance”
三、补:对象的一些基本知识
1. object.assign()
这个方法接收一个目标对象和一个或者多个源对象作为参数,然后将每个源对象中可枚举和自有的属性复制到目标对象,以字符串和symbol为键的属性会被复制。
- 实际上对每个对象执行的是浅复制,如果多个源对象都具有相同的值,那么就使用最后一个复制的值。
2. 创建对象
-
工厂模式(
new Object实现,并用函数封装)- 可以解决创建多个类似对象的问题,但是没有解决新创建的对象是什么类型的问题。
-
构造函数模式
- 此时使用new操作符会执行的步骤同上方使用class一致
3. 理解原型
- 只要创建一个函数,就会为这个函数创建一个
prototype属性,该属性指向了原型对象。 - 默认情况下所有原型对象获得一个
constructor属性,指回与之关联的构造函数。 - 每次调用构造函数创建一个实例,实例内部的隐式原型,即prototype指针(
__proto__)都会被赋值为构造函数的原型对象。 - 实例和其构造函数并无直接的指向关系。
- 通过对象来访问属性的时候,会按照属性名称进行查找,查找始于对象实例本身,如果没有找到则会沿着
__proto__进入原型对象进行查找。 - 如果在实例上添加了和原型对象上同名的属性,那么这个属性就会遮蔽(shadow)原型对象上的同名属性(不会修改,但是会屏蔽对其的访问)
hasOwnProperty()用于判断一个属性是实例属性还是原型属性。
4.对象迭代
-
单独使用
in进行判断指定属性是否存在时,既查看实例属性又查看原型属性。 -
使用
for-in循环,返回所有可以通过对象访问且可以被枚举的属性(包括实例属性和原型属性),遮蔽原型中不可枚举属性的实例属性也会被返回(默认开发者定义的属性都是可枚举的) -
object.keys()返回该对象所有可枚举的实例属性名称的字符串数组 -
枚举的顺序也有所差异:
for-in和object.keys顺序不确定assign, getOwnPropertyNames(), getOwnPropertySymbols()首先以升序枚举数值键,然后以插入顺序枚举字符串和符号键
-
object.values()返回value数组,object.entries()返回键值对数组。非字符串属性会被转换成字符串属性输出,这两个方法会执行浅复制。