抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Yuki 妙妙屋

不要因为走得太远,就忘了当初为什么出发。

在 JavaScript(以下简称 JS)中,面向对象编程有着使用 function 构造函数或 class 来配合 new 生成实例对象的两种方式。

尽管我们知道,传统构造函数是 ES5 中的写法,而 class 则是在 ES6 中新引入的关键字,那么这两种方式到底有什么区别?

class 的由来

在 ES5 中,与传统的面向对象语言(比如 C++ 和 Java)相比,JS 在生成实例对象的写法上有着很大的不同。

例如下面的例子:

1
2
3
4
5
6
7
8
9
10
function Phone(brand) {
this.brand = brand;
}

Phone.prototype.call = function () {
console.log('The "hello world" from %s.', this.brand);
};

var xiaomi = new Phone('xiaomi');
xiaomi.call(); // -> The "hello world" from xiaomi.
为什么要拿手机来举例子?嗯……随手写的,并没有什么特别深的含义。 Do you guys not have phones?

而 ES6 提供了更接近传统语言的写法,引入了类的概念。上面的代码用 class 关键字来编写,就是下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
class Phone {
constructor(brand) {
this.brand = brand;
}

call() {
console.log(`The "hello world" from ${this.brand}.`);
}
}

const huawei = new Phone('huawei');
huawei.call(); // -> The "hello world" from huawei.

可以看到,尽管在写法上有所差异,但上述代码的表现行为是完全一致的。我们可以将 class 看作是构造函数的一个语法糖,不过使用 class 能让代码逻辑更加清晰,使其看起来更像面向对象编程的语法。

语法糖,但不完全语法糖

很多开发者在学习初期,会认为 class 仅仅是一个语法糖,因为在不使用它的情况下,完全可以用 function 来实现同样的效果。不过,这两者并不能完全画等号。

我们通过 class 创建的,是一种特殊的函数对象,其内部的 [[FunctionKind]] 属性被标记为了 "classConstructor",而 JS 引擎会在许多地方检查 [[IsClassConstructor]] 属性。

虽然我们无法直接在代码里观察到这些内部槽(internal slots),但可以通过其表现行为来佐证。

调用方式不同

function 不同,class 必须配合 new 使用,而构造函数是普通函数,不使用 new 也能直接调用。

1
2
3
4
5
6
7
8
9
class Smartphone {
static crackWalnuts() {
console.log('You are sure?');
}
}

// Uncaught TypeError: Class constructor Smartphone cannot be invoked without 'new'
Smartphone();
Smartphone.crackWalnuts();
1
2
3
4
5
6
7
8
function Dumbphone() {}

Dumbphone.crackWalnuts = function () {
console.log('Crack!');
};

Dumbphone();
Dumbphone.crackWalnuts(); // -> Crack!

可以看到,尽管 Dumbphone 添加了静态方法,但它依然可以作为函数正常执行,不受任何影响。

严格模式

使用 class 创建的类,其内部代码默认启用严格模式。构造函数、静态方法、原型方法、getter 和 setter,都是在严格模式下强制执行,无法手动关闭。

1
2
3
4
5
6
7
function Dumbphone() {
with (console) {
log('I am Dumbphone.');
}
}

new Dumbphone(); // -> "I am Dumbphone."
1
2
3
4
5
6
7
8
class Smartphone {
constructor() {
// Uncaught SyntaxError: Strict mode code may not include a with statement
with (console) {
log('I am Smartphone.');
}
}
}

方法不可枚举

使用 class 会将其原型上的方法的 enumerable 标志定义为 false,使得它们不可被枚举,而通过给 prototype 赋值的方式定义的方法默认是可枚举的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Dumbphone() {}

Dumbphone.prototype.openCover = function () {
console.log('打开后盖就能看到我的电池,咱还能再装回去 (*/ω\*)');
};

class Smartphone {
openCover() {
console.log('杂鱼❤杂鱼❤~打不开后盖的杂鱼~没有充电器就只能等着关机的杂鱼~');
}
}

console.log(Dumbphone.prototype); // -> { constructor: ƒ, openCover: ƒ }
console.log(Smartphone.prototype); // -> { constructor: ƒ, openCover: ƒ }
console.log(Object.keys(Dumbphone.prototype)); // -> ['openCover']
console.log(Object.keys(Smartphone.prototype)); // -> []
console.log(Object.getOwnPropertyNames(Smartphone.prototype)); // -> ['constructor', 'openCover']

function 的继承

在看过了前面的内容,相信你对 JS 的类已经有了一个大致的了解。但这个时候你一定有所疑问,构造函数是如何做到继承的?

呐,构造函数的继承有四样写法,你知道么? 我教给你,记着!这些应该记着。将来做程序员的时候,开发要用。

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Phone(brand) {
this.brand = brand;
}

Phone.prototype.call = function () {
console.log('The "hello world" from %s.', this.brand);
};

function XiaomiPhone(model) {
this.model = model;
}

XiaomiPhone.prototype = new Phone('Xiaomi');
XiaomiPhone.prototype.constructor = XiaomiPhone;
XiaomiPhone.prototype.slogan = function () {
console.log('为发烧而生');
};

var xiaomi = new XiaomiPhone('13 Ultra');

思路:

  1. 将子类的原型对象指向父类的实例对象。

缺点:

  1. 子类实例化时无法为父类构造函数传入不同参数。
  2. 子类原型中包含的引用值会在所有实例之间共享。

构造函数继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Phone(brand, price) {
this.brand = brand;
this.price = price;

this.call = function () {
console.log('The "hello world" from %s.', this.brand);
};
}

function XiaomiPhone(model, price) {
Phone.call(this, 'Xiaomi', price);
this.model = model;
}

XiaomiPhone.prototype.slogan = function () {
console.log('为发烧而生');
};

var xiaomi = new XiaomiPhone('13 Ultra', 5999);

思路:

  1. 让父类构造函数在子类内部运行。
  2. 将父类内部的 this 指向子类的实例。

缺点:

  1. 父类方法必须定义在构造函数中。
  2. 子类不能访问父类原型上定义的属性与方法。

组合继承(经典写法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Phone(brand, price) {
this.brand = brand;
this.price = price;
}

Phone.prototype.call = function () {
console.log('The "hello world" from %s.', this.brand);
};

function XiaomiPhone(model, price) {
Phone.call(this, 'Xiaomi', price);
this.model = model;
}

XiaomiPhone.prototype = Object.create(Phone.prototype);
XiaomiPhone.prototype.constructor = XiaomiPhone;
XiaomiPhone.prototype.slogan = function () {
console.log('为发烧而生');
};

var xiaomi = new XiaomiPhone('13 Ultra', 5999);

这种方式综合了「构造函数继承」和「原型链继承」的优点,是 ES5 中最经典的继承方式(之一)。

关于构造函数的继承方式,我们可以在网上看到有很多种写法,但从严格意义上来讲,我个人认为其继承机制,基本归纳为上述三种。其它常见的各种写法,都是以上三类方式的变体或组合。例如寄生继承,就是在这其中结合了函数工厂模式。

class extends

在 ES6 发布之后,类的继承有了质的提升。如果熟悉别的编程语言,那么对 extends 这个关键字一定不会陌生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Phone {
constructor(brand, price) {
this.brand = brand;
this.price = price;
}

call() {
console.log(`The "hello world" from ${this.brand}.`);
}
}

class XiaomiPhone extends Phone {
constructor(model, price) {
super('Xiaomi', price);
this.model = model;
}

slogan() {
console.log('为发烧而生');
}
}

const xiaomi = new XiaomiPhone('13 Ultra', 5999);

泰裤辣!

extends

评论