01、前言
众所周知,JavaScript 是一门面向对象的语言,而构造函数、原型、类、继承都是与对象密不可分的概念。在我们日常前端业务开发中,系统和第三方库已经为我们提供了大部分需要的类,我们的关注点更多是在对象的使用和数据处理上,而比较少需要去自定义构造函数和类,对原型的直接接触就更少了。
然而,能深度理解并掌握好构造函数、原型、类与继承,对我们的代码设计大有裨益,也是作为一名高级前端工程师必不可少的基本功。
本文旨在用最通俗易懂的解释和简单生动的代码示例,来彻底捋清对象、构造函数、原型、类与继承。我们会以问答对话的形式,层层递进,从构造函数谈起,再引出原型与原型链,分析类为什么是语法糖,最后再推理出 JS 的几种继承方式。
在进入正式篇章之前,我们可以先尝试思考以下几个问题:
1.new Date().__proto__ == Date.prototype?
2.new Date().constructor == Date?
3.Date.__proto__ == Function.prototype?
4.Function.__proto__ == Function.prototype?
5.Function.prototype.__proto__== Object.prototype?
6.Object.prototype.__proto__ == null?
—— 思考分割线 ——
没错,它们都是 true !为啥?听我娓娓道来~
02、构造函数
某IT公司前端研发部,新人小Q和职场混迹多年的老白聊起着构造函数、原型与类的话题。
小Q:构造函数我知道呀,平时 new Date(),new Promise() 经常用, Date,Promise 不就是构造函数,我们通过 new 一个构造函数去创建并返回一个新对象。
老白:没错,这些是系统自带的一些构造函数,那你可以自己写个构造函数吗?
小Q:虽然平时用的不多,但也难不倒我~
// 定义个构造函数
function Person(name) {
this.name = name;
}
// new构造函数,创建对象
let person = new Person("张三");
小Q:看吧 person 就是对象,Person 就是构造函数,清晰明了!
老白:那我要是单纯写这个方法算不算构造函数?
function add(a, b) {
return a + b;
}
小Q:这不是吧,这明显就是个普通函数啊?
老白:可是它也可以 new 对象哦!
function add(a, b) {
return a + b;
}
let a = new add(1, 2);
// add {}
console.log(a);
// true
console.log(a instanceof add);
// object
console.log(typeof a);
小Q:诶?
老白:其实所谓构造函数,就是普通函数,关键看你要不要 new 它,但是 new 是在使用的时候,在定义的时候咋知道它后面会不会被 new 呢,所以构造函数只不过是当被用来new 时的称呼。就像你上面的 Person 函数,不要 new 直接运行也是可以的嘛。
function Person(name) {
this.name = name;
}
Person("张三");
小Q:哦,我懂了,所有函数都可以被 new,都可以作为构造函数咯,所谓构造函数只是一种使用场景。
老白:嗯嗯,总结得很好,但也不全对,比如箭头函数就不能被 new,因为它没有自己的 this 指向,所以不能作为构造函数。比如下面这样就会报错。
let Person = (name) => {
this.name = name;
};
// Uncaught TypeError: Person is not a constuctor
let person = new Person("张三");
小Q:原来如此,那你刚刚 Person("张三"); ,既然没有创建新对象,那里面的 this 又指向谁了?
老白:这就涉及到函数内 this 指向问题了,可以简单总结以下 5 种场景。
1. 通过 new 调用,this 指向创建的新对象;
2. 直接当做函数调用,this 指向 window(严格模式下为 undefined);
function Person(name) {
this.name = name;
}
// this 指向 window
Person("张三");
// 张三
console.log(window.name);
(看吧,不注意的话,不小心把 window 对象改了都不知道)
3.作为对象的方法调用,this 指向该对象;
function Person(name) {
this.name = name;
}
let obj = {
Person,
};
// this 指向 obj
obj.Person("张三");
// { "name": "张三", Person: f }
console.log(obj);
4.通过 apply,call,bind 方法,显式指定 this;
function Person(name) {
this.name = name;
}
// this 指向 call 的第一个参数
Person.call(Math, "张三");
// 张三
console.log(Math.name);
5.箭头函数中没有自己的 this 指向,取决于上下文:
function Person(name) {
this.name = name;
// 普通函数,this 取决于调用者,即上述的 4 种情况
setTimeout(function() {
console.log(this);
}, 0)
// 箭头函数,this 取决于上下文,我们可以忽略箭头函数的存在
// 即同上面 this.name = name 中的 this 指向一样
setTimeout(() => {
console.log(this);
}, 0)
}
小Q:原来 this 指向都有这么多种情况,好的,小本本记下了,等下就去试验下。
小Q:等下,我重新看了你的 new add(1, 2),那 a + b = 3 还被 return 了呢,这 3 return 到哪去了?
function add(a, b) {
return a + b;
}
let a = new add(1, 2);
老白:没错,你注意到了,构造函数是不需要 return 的,函数中的 this 就是创建并返回的新对象了。
但当 new 一个有 return 的构造函数时,如果 return 的是基本类型,则 return 的数据直接被抛弃。
如果 return 一个对象,则最终返回的新对象就是 return 的这个对象,这时原本 this 指向的对象就会被抛弃。
function Person(name) {
this.name = name;
// 返回的是对象类型
return new Date();
}
let person = new Person("张三");
// 返回的是 Date 对象
// Sat Jul 29 2023 16:13:01 GMT+0800 (中国标准时间)
console.log(person);
老白:当然如果要把一个函数的使用用途作为构造函数的话,像我刚刚起名 add() 肯定是不规范的, 一般首字母要大写,并且最好用名词,像你起的 Person 就不错。
小Q:新知识get√
要点归纳 |
1. 除箭头函数外的所有函数都可以作为构造函数被new |
2. 函数内this指向问题 |
3. 构造函数return问题 |
4. 构造函数命名规范 |
03、原型
小Q:都说原型原型,可看了这么久,这代码里也没出现原型呀?
老白:没错,原型是个隐藏的家伙,我们可以通过对象或者构造函数去拿到它。
// 构造函数
function Person(name) {
this.name = name;
}
// 对象
let person = new Person("张三");
// 通过对象拿到原型(2种方法)
let proto1 = Object.getPrototypeOf(person);
let proto2 = person.__proto__;
// 通过构造函数拿到原型
let proto3 = Person.prototype;
// 验证一下
// true
console.log(proto1 == proto2);
// true
console.log(proto1 == proto3);
小Q:可是这个原型是哪来的呀,我代码里也没创建它呀?
老白:当你声明一个函数时,系统就自动帮你生成了一个关联的原型啦,当然它也是一个普通对象,包含 constructor 字段指向构造函数,并且构造函数的 prototype 属性也会指向这个原型。
当你用构造函数创建对象时,系统又帮你把对象的 __proto__ 属性指向原型。
// 构造函数
function Person(name) {
this.name = name;
}
// 可以理解为:声明函数时,系统自动执行了下面代码
Person.prototype = {
// 指向构造函数
constructor: Person
}
// 对象
let person = new Person("张三");
// 可以理解为:创建对象时,系统自动执行了下面代码
person.__proto__ == Person.prototype;
小Q:它们的引用关系,稍微有点绕啊~
老白:没事,我画两个图来表示,更加清晰点。
(备注:proto 只是单纯用来表示原型的一个代名而已,代码中并不存在)
图片
图片
小Q:懂了!
老白:那你说说 {}.__proto__ 和 {}.consrtuctor 分别是什么?
小Q:让我分析下,{} 其实就是 new Object() 的一种字面量写法,本质上就是 Object 对象,那 {}.__proto__ 就是原型 Object.prototype,{}.constructor 就是构造函数 Object,对吧?
老白:没错,只要能熟练掌握上面这个图,构造函数,原型和对象这三者的引用关系基本很清晰了。一开始提的1、2 题基本也迎刃而解了!
- new Date().__proto__ == Date.prototype ?
- new Date().constructor == Date ?
小Q:那这个原型有什么用呢?
老白:一句话总结:当访问对象的属性不存在时,就会去访问原型的属性。
图片
图3
老白:我们可以通过代码验证下,person 对象是没有 age 属性的,所以 person.age 返回的其实是原型的 age 属性值,当原型的 age 属性改变时,person.age 也会跟着改变。
function Person(name) {
this.name = name;
}
// 给原型增加age属性
Person.prototype.age = 18;
// 对象
let person = new Person("张三");
// 18
console.log(person.age);
// 修改原型的age属性
Person.prototype.age++;
// 19
console.log(person.age);
小Q:那如果我直接 person.age++ 呢,改的是 person 还是原型?
老白:这样的话就相当于 person.age = person.age + 1 啦,等号右边的 person.age 因为 对象目前还没 age 属性,所以拿到的是原型的 age 属性,即18,然后 18 + 1 = 19 将赋值给 person 对象。
后续当你再访问 person.age 时,因为 person 对象已经存在 age 属性了,就不会再检索到原型上了。
这种行为我们一般称为重写,在这个例子里也描述为:person 对象重写了原型上的 age 属性。
图片
图4
小Q:那这样的话使用起来岂不是很乱,我还得很小心的分析 person.age 到底是 person 对象的还是原型的?
老白:没错,如果你不想出现这种无意识的重写,将原型上的属性设为对象类型不失为一种办法。
function Person(name) {
this.name = name;
}
// 原型的info属性是对象
Person.prototype.info = {
age: 18,
};
let person = new Person("张三");
person.info.age++;
小Q:我懂了,改变的是 info 对象的 age 属性, person 并没有重写 info 属 性,所以 person 对象本身依然没有 info 属性,person.info 依然指向原型。
老白:没错!不过这样也有个坏处,每一个 Person 对象都可以共享原型的 info ,当 info 中的属性被某个对象改变了,也会对其他对象造成影响。
function Person(name) {
this.name = name;
}
Person.prototype.info = {
age: 18,
};
let person1 = new Person("张三");
let person2 = new Person("李四");
// person1修改info
person1.info.age = 19;
// person2也会被影响,打印:19
console.log(person2.info.age);
老白:这对我们代码的设计并不好,所以我们一般不在原型上定义数据,而是定义函数,这样对象就可以直接使用挂载在原型上的这些函数了。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log("hello");
}
let person = new Person("张三");
// hello
person.sayHello();
小Q:我理解了,数据确实不应该被共享,每个对象都应该有自己的数据好点,但是函数无所谓,多个对象可以共享同一个原型函数。
老白:所以你知道为啥 {} 这个对象本身没有任何属性,却可以执行 toString() 方法吗?
小Q:【恍然大悟】来自它的原型 Object.prototype !
老白:不仅如此,很多系统自带的构造函数产生的对象,其方法都是挂载在原型上的。比如我们经常用的数组方法,你以为是数组对象自己的方法吗?不,是数组原型 Array.prototype 的方法,我们可以验证下。
let array = [];
// array对象的push和原型上的push是同一个
// 打印:true
console.log(array.push == Array.prototype.push);
// array对象本身没有自己的push属性
// 打印:false
console.log(array.hasOwnProperty("push"));
图片
小Q:【若有所思】
老白:再比如,你随便定义一个函数 function fn() {},为啥它就能 fn.call() 这样执行呢,它的 call 属性是哪来的?
小Q:来自它的原型?函数其实是 Function 的对象,那它的原型就是 Function.prototype,试验一下。
function fn() {}
// true
console.log(fn.constructor == Function);
// true
console.log(fn.call == Function.prototype.call);
老白:回答正确。在实际开发中,我们也可以通过修改原型上的函数,来改变对象的函数执行。比如说我们修改数组原型的 push 方法,加个监听,这样所有数组对象执行 push 方法时就能被监听到了。
Array.prototype.push = (function (push) {
// 闭包,push是原始的那个push方法
return function (...items) {
// 执行push要指定this
push.call(this, ...items);
console.log("监听push完成,执行一些操作");
};
})(Array.prototype.push);
let array = [];
// 打印:监听push完成,执行一些操作
array.push(1, 2);
// 打印:[1, 2]
console.log(array);
老白:不只修改,也可以新增,比如说某些旧版浏览器数组不支持 includes 方法,那我们就可以在原型上新增一个 includes 属性,保证代码中数组对象使用 includes() 不会报错(这也是 Polyfill.js 的目的)。
// 没有includes
if(!Array.prototype.includes) {
Array.prototype.includes = function() {
// 自己实现includes
}
}
小Q:又又涨知识了~
老白:原型相关的也说的差不多了,结合刚刚讨论的构造函数,考你一个:手写一个 new 函数。
小Q:啊啊,提示一下?
老白:好,我们简单分析一下 new 都做了什么
- 创建一个对象,绑定原型;
- 以这个对象为 this 指向执行构造函数。
小Q:我试试~
function myNew(Fn, ...args) {
var obj = {
__proto__: Fn.prototype,
};
Fn.apply(obj, args);
return obj;
}
小Q:试验通过!
// 构造函数
function Person(name) {
this.name = name;
}
// 原型
Person.prototype.age = 18;
// 创建对象
let person = myNew(Person, "张三");
// Person {name: "张三"}
console.log(person);
// 18
console.log(person.age);
老白:不错不错,让我帮你再稍微完善一下嘿嘿~
function myNew(Fn, ...args) {
// 通过Object.create指定原型,更加符合规范
var obj = Object.create(Fn.prototype);
// 指定this为obj对象,执行构造函数
let result = Fn.apply(obj, args);
// 判断构造函数的返回值是否是对象
return result instanceof Object ? result : obj;
}
要点归纳 |
1. 对象,构造函数,原型三者的引用关系 |
2. 原型的定义,特性及用法 |
3. 手写new函数 |
04、原型链
老白:刚刚我们说当访问对象的属性不存在时,就会去访问原型的属性,那假如原型上的属性也不存在呢?
小Q:返回 undefined?
老白:不对哦,原型本身也是一个对象,它也有它自己的原型。所以当访问一个对象的属性不存在时,就会检索它的原型,检索不到就继续往上检索原型的原型,一直检索到根原型 Object.prototype,如果还没有,才会返回 undefined,这也称为原型链。
图片
小Q:原来如此,所以说所有的对象都可以使用根原型 Object.prototype 上定义的方法咯。
老白:没错,不过有一些原型会重写根原型上的方法,就比如 toString(),在 Date.prototype,Array.prototype 中都会有它们自己的定义。
// [object Object]
console.log({}.toString())
// 1,2,3
console.log([1,2,3].toString())
// Tue Aug 01 2023 17:58:05 GMT+0800 (中国标准时间)
console.log(new Date().toString())
小Q:理解了原型链,看回开始的3~6题,好像也不难了。
Date、Function 的原型是 Function.prototype,第 3、4 题就解了。
Function.prototype 的原型是 Object.prototype,第 5 题也解了。
Object.prototype 是根原型,所以它的 __proto__ 属性就为 null,第 6 题也解了。
- Date.__proto__ == Function.prototype ?
- Function.__proto__ == Function.prototype ?
- Function.prototype.__proto__== Object.prototype ?
- Object.prototype.__proto__ == null ?
老白:完全正确。最后再考你一道和原型链相关的题,手写 instanceOf 函数。提示一下,instanceOf 的原理是判断构造函数的 prototype 属性是否在对象的原型链上。
// array的原型链:Array.prototype → Object.prototype
let array = [];
// true
console.log(array instanceof Array);
// true
console.log(array instanceof Object);
// false
console.log(array instanceof Function);
小Q:好了嘞~
function myInstanceof(obj, Fn) {
while (true) {
obj = obj.__proto__;
// 匹配上了
if (obj == Fn.prototype) {
return true;
}
// 到达原型链的尽头了
if (obj == null) {
return false;
}
}
}
检测一下:
let array = [];
// true
console.log(myInstanceof(array, Array));
// true
console.log(myInstanceof(array, Object));
// false
console.log(myInstanceof(array, Function));
老白:Good!
要点归纳 |
1. 原型链 |
2. 手写 instanceOf函数 |
05、类
小Q:好不容易把构造函数和原型都弄懂,怎么 ES6 又推出类呀,学不动了 T_T。
老白:不慌,类其实只是种语法糖,本质上还是”构造函数+原型“。
我们先看一下类的语法,类中可以包含有以下4种写法不同的元素。
- 对象属性:key = xx
- 原型属性:key() {}
- 静态属性:static key = x 或 static key() {}
- 构造器:constructor() {}
class Person {
// 对象属性
a = "a";
b = function () {
console.log("b");
};
// 原型属性
c() {
console.log("c");
}
// 构造器
constructor() {
// 修改对象属性
this.a = "A";
// 新增对象属性
this.d = "d";
}
// 静态属性
static e = "e";
static f() {
console.log("f");
}
}
我们再将这种 class 语法糖写法还原成构造函数写法。
function Person() {
// 对象属性
this.a = "a";
this.b = function () {
console.log("b");
};
// 构造器
this.a = "A";
this.d = "d";
}
// 原型属性
Person.prototype.c = function () {
console.log("c");
};
// 静态属性
Person.e = "e";
Person.f = function () {
console.log("f");
};
通过下面一些方法检测,上面的2种写法会得到同样的结果。
// Person类本质是个构造函数,打印:function
console.log(typeof Person);
// Person的静态属性,打印:e
console.log(Person.e);
// 可以看到原型属性c,打印:{constructor: ƒ, c: ƒ}
console.log(Person.prototype);
let person = new Person();
// 可以看到对象属性a b d,打印:Person {a: 'A', d: 'd', b: ƒ}
console.log(person);
// 对象的构造函数就是Person,打印:true
console.log(person.constructor == Person);
小Q:所以类只不过是将本来比较繁琐的构造函数的写法给简化了而已,这语法糖果然甜~
小Q:不过我发现一个问题,在 class 写法中的原型属性只能是函数,不能是数据?
老白:没错,这也呼应了前面说的,原型上只推荐定义函数,不推荐定义数据,避免不同对象共享同一个数据。
要点归纳 |
1. 类的语法 |
2. 类还原成构造函数写法 |
06、继承
小Q:我又又发现了一个问题,ES6 的 class 还可以 extends 另一个类呢,这也是语法糖?
老白:没错,这就是继承,但是要弄懂 ES6 的这套继承是怎么来的,还得从最开始的继承方式说起。所谓继承,就是我们是希望子类可以拥有父类的属性方法,这和上面谈到的原型的特性有点不谋而合。
我们用一个例子来思考思考,有这么 2 个类,如何让 Cat 继承 Animal,使得 Cat 的对象也有 type 属性呢?
// 父类
function Animal() {
this.type = "动物";
}
// 子类
function Cat() {
this.name = "猫";
}
小Q:让 Animal 对象充当 Cat 的原型!
function Animal() {
this.type = "动物";
}
function Cat() {
this.name = "猫";
}
// 指定Cat的原型
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
let cat = new Cat();
// Cat对象拥有了Animal的属性
console.log(cat.type);
老白:没错,这是我们学完原型之后,最直观的一种继承实现方式,这种继承又叫原型链式继承。但是这种继承方式存在 2 个缺点:
- 父类对象作为原型,其属性会被所有子类对象共享;
- 创建子类对象时无法向父类构造函数传参。
function Animal(type) {
this.type = type;
}
function Cat(type) {
this.name = "猫";
}
// 在这里就已经创建了Animal对象
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 创建子类对象时无法向父类构造函数传参
let cat = new Cat("哺乳动物");
// type属性来自原型,被所有Cat对象共享,打印:undefined
console.log(cat.type);
小Q:我想到个办法,可以一举解决上面2个缺点。
在子类构造函数中执行父类构造函数,并且指定执行父类构造函数中的 this 是子类对象,这样属性就都是属于子类对象本身了,不存在共享。同时在创建子类对象时,也可以给父类构造函数传参了,一举两得。
function Animal(type) {
this.type = type;
}
function Cat(type) {
// 执行父类,显式指定this就是子类的对象
Animal.call(this, type);
this.name = "猫";
}
let cat = new Cat("哺乳动物");
// Cat {type: '哺乳动物', name: '猫'}
console.log(cat);
老白:这种继承方式叫 构造函数式继承,确实解决了 原型链式继承 带来的问题,不过这种继承方式因为没有用到原型,又有产生了2个新的问题:
- 没有继承父类原型的属性方法;
- 子类对象不是父类的实例。
function Animal(type) {
this.type = type;
}
// 父类的原型方法
Animal.prototype.eat = function () {
console.log("吃");
};
function Cat(type) {
Animal.call(this, type);
this.name = "猫";
}
let cat = new Cat("哺乳动物");
// 没有继承父类原型的属性方法,打印:undefined
console.log(cat.eat);
// 子类对象不是父类的实例,打印:false
console.log(cat instanceof Animal);
小Q:看来还要再改进,不如我把 原型链式 和 构造函数式 这 2 种继承方式都用上,让它们互补。
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function () {
console.log("吃");
};
// 子类构造函数
function Cat(type) {
Animal.call(this, type);
this.name = "猫";
}
// 父类对象充当子类原型
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
试验一下,果然所有问题都解决了。
// 可以给父类构造函数传参
let cat = new Cat("哺乳动物");
// 子类对象拥用自己属性,而非来自原型,避免数据共享
// 打印:Cat {type: '哺乳动物', name: '猫'}
console.log(cat);
// 子类对象可以继承到父类原型的方法,打印:吃
cat.eat();
// 子类对象属于父类的实例,打印:true
console.log(cat instanceof Animal);
老白:非常聪明,你又道出了第三种继承方式,组合式继承。即 原型链式 + 构造函数式 = 组合式。问题确实都解决了,但是有没有发现,这种方式执行了 2 遍父类构造函数。
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function () {
console.log("吃");
};
function Cat(type) {
// 第二次执行父类构造函数
Animal.call(this, type);
this.name = "猫";
}
// 第一次执行父类构造函数
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
小Q:多执行了一遍,确实不够完美,这怎么搞?
老白:其实关键在 Cat.prototype = new Animal(),你只不过想让子类对象也能继承到父类的原型,而这里创建了一个父类对象,为啥?说到底还是利用原型链: 子类对象 → 父类对象 → 父类原型。
如果我们不要中间那个"父类对象",而是用一个“空对象x”替换,让原型链变成:子类对象 → 空对象x → 父类原型,这样也能达到目的,就不用执行那遍没必要的父类构造函数了。
// 组合式继承:创建父类对象做子类原型
let animal = new Animal();
Cat.prototype = animal;
// 改进:创建一个空对象做子类原型,并且这个空对象的原型是父类原型
let x = Object.create(Animal.prototype);
Cat.prototype = x;
小Q:妙啊,这回完美了。
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function () {
console.log("吃");
};
function Cat(type) {
Animal.call(this, type);
this.name = "猫";
}
// 寄生组合式,改进了组合式,少执行了一遍没必要的父类构造函数
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
老白:这种继承方式又叫 寄生组合式继承,相当于在组合式继承的基础上进一步优化。回顾上面的几种继承方式的演变过程,原型链式 → 构造函数式 → 组合式 → 寄生组合式, 其实就是不断优化的过程,最终我们才推理出比较完美的继承方式。
小Q:那 ES6 class 的 extends 继承 又是怎样呢?
老白:说到底就是 寄生组合式继承 的语法糖。我们先看看它的语法。
class Animal {
eat() {
console.log("吃");
}
constructor(type) {
this.type = type;
}
}
// Cat继承Animal
class Cat extends Animal {
constructor(type) {
// 执行父类构造函数,相当于 Animal.call(this, type);
super(type);
// 执行完super(),子类对象就有父类属性了,打印:哺乳动物
console.log(this.type);
this.name = "猫";
}
}
创建对象试验一下:
let cat = new Cat("哺乳动物");
// 子类原型的原型就是父类原型,打印:true
console.log(Cat.prototype.__proto__ == Animal.prototype);
// 子类本身拥有父类的属性,打印:Cat {type: '哺乳动物', name: '猫'}
console.log(cat);
打印的结果展示的特性和 寄生组合式 是一样的:
- 子类原型的原型就是父类原型;
- 子类本身拥有父类的属性。
特性1 可以理解为 extends 背地里执行了:
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
特性2 在于 super(),它相当于 Animal.call(this),执行 super() 就是执行父类构造函数,将原本父类中的属性都赋值给子类对象。
在 ES6 的语法中还要求 super() 必须在 this 的使用前调用,也是为了保证父类构造函数先执行,避免在子类构造器中设置的 this 属性被父类构造函数覆盖。
class Animal {
constructor() {
// 假如不报错,this.name = "猫" 就被 this.name= "狗" 覆盖了
this.name = "狗";
}
}
class Cat extends Animal {
constructor(type) {
this.name = "猫";
// 没有在this使用前调用,报错
super();
}
}
小Q:看懂 寄生组合式继承, extends 继承 就是小菜一碟呀~
老白:最后再补充一下 super 的语法,可以子类的静态属性方法中通过 super.xx 访问父类静态属性方法。
class Animal {
constructor() {}
static num = 1;
static say() {
console.log("hello");
}
}
class Cat extends Animal {
constructor() {
super();
}
// super.num 相当于 Animal.num
static count = super.num + 1;
static talk() {
// super.say() 相当于 Animal.say()
super.say();
}
}
// 2
console.log(Cat.count);
// hello
Cat.talk();
super 是一个语法糖的特殊关键词,特殊用法,并不指向某个对象,不能单独使用,以下情况都是不允许的。
class Animal {}
class Cat extends Animal {
constructor() {
// 报错
let _super = super;
// 报错
console.log(super);
}
static talk() {
// 报错
console.log(super);
}
}
要点归纳
- 原型链式继承
- 构造函数式继承
- 组合式继承
- 寄生组合式继承
- extends 继承
07、总结
本文深入浅出地讨论了 JavaScript 构造函数、原型、类、继承的特性和用法,以及它们之间的关系。希望看完本文,能帮助大家对它们有更加清晰通透的认识和掌握!