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

Yuki 妙妙屋

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

在 JavaScript 中,生成实例对象有着 new functionnew class 两种写法。

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

class 的由来

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

例如下面的例子:

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 %s.', this.brand);
}
}

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

可以看到,尽管在写法上有所差异,但上述代码的表现行为是完全一致的。基本上 ES6 的类可以看作是函数的一个语法糖,不过,虽然函数几乎可以实现类的所有特性表现,但使用 class 能让对象原型的写法更加清晰,使其看起来更像面向对象编程的语法。

语法糖,但不完全语法糖

在大部分人的认知里,class 仅仅只是一个语法糖,因为在不使用它的情况下,完全可以用 function 实现同样的效果。

不过,这两者间并不能完全划等号,它们也有着些许不同。通过 class 创建的函数,其具有特殊的内部属性标记 [[IsClassConstructor]]: true,JavaScript 引擎会在许多地方检查该属性。

虽然我们无法直接在代码层面目睹到 IsClassConstructor 的存在,但我们可以使用其它方式去一一佐证。

调用方式不同

例如,与 function 不同,class 必须使用 new 来调用它。这是它与普通构造函数的一个主要区别,前者不用 new 也可以执行。

1
2
3
4
5
6
7
8
class Pen {
constructor() {
console.log('I have a pen.');
}
}

// Uncaught TypeError: Class constructor Pen cannot be invoked without 'new'
Pen();
1
2
3
4
5
function Apple() {
console.log('I have a apple, uhh~ wait, where is my pen?');
}

Apple(); // -> I have a apple, uhh~ wait, where is my pen?

严格模式

使用 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.');
}
}
}

不可枚举

classprototype 中的所有方法的 enumerable 标志设置为了 false,这使得它们不可被枚举。

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

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

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

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

实例化

虽然列举了这么多不同点,不过不管是 class 还是 function,它们的实例化对象在表现上都是一致的。

原型链结构

例如,类的属性和方法,除非显式定义在其本身 this 上,否则都是定义在 __proto__ 原型链上。

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

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

const huawei = new Phone('huawei');

huawei.call(); // -> The "hello world" from huawei.
huawei.hasOwnProperty('brand'); // -> true
huawei.hasOwnProperty('call'); // -> false
huawei.__proto__.hasOwnProperty('call'); // -> true

上面代码中,brand 是实例对象 huawei 自身的属性(因为定义在 this 对象上),所以 hasOwnProperty() 方法返回 true,而 call() 是原型对象的属性(因为定义在 Phone 类上),所以 hasOwnProperty() 方法返回 false。这些都与 ES5 的行为保持一致。

共享原型对象

与 ES5 一样,类的所有实例共享一个原型对象。

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

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

const xiaomi = new Phone('xiaomi');
const huawei = new Phone('huawei');

console.log(xiaomi.__proto__ === huawei.__proto__); // -> true

上面代码中,xiaomihuawei 都是 Phone 的实例,它们的原型都是 Phone.prototype,所以 __proto__ 属性是相等的。

这也意味着,可以通过实例的 __proto__ 属性为当前类添加方法。

1
2
3
4
5
6
7
8
9
10
const xiaomi = new Phone('xiaomi');
const huawei = new Phone('huawei');

xiaomi.__proto__.camera = () => console.log('LYCRA is so cooooool ♪(o∀o)っ');

xiaomi.camera(); // -> "LYCRA is so cooooool ♪(o∀o)っ"
huawei.camera(); // -> "LYCRA is so cooooool ♪(o∀o)っ"

const nokia = new Phone('nokia');
nokia.camera(); // -> "LYCRA is so cooooool ♪(o∀o)っ"

上面代码在 xiaomi 的原型上添加了一个 camera() 方法,由于 xiaomi 的原型就是 huawei 的原型,因此 huawei 也可以调用这个方法。

而且,此后新建的实例 nokia 也可以调用这个方法。这意味着,使用实例的 __proto__ 属性改写原型,必须相当谨慎,不推荐使用,因为这会改变类的原始定义,影响到所有实例。不是所有相机都叫莱卡

function 的继承

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

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

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.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
22
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) {
this.brand = 'Xiaomi';
this.model = model;
this.price = price;
}

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

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

思路:

  1. 让子类的原型指向父类的原型

缺点:

  1. 子类会修改父类的原型对象,造成原型链结构混乱

寄生继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function createPhone(object) {
function Phone() {}

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

function createXiaomiPhone(model, price) {
var phone = createPhone({
brand: 'Xiaomi',
price: price,
});

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

return phone;
}

var xiaomi = createXiaomiPhone('13 Ultra', 5999);

思路:

  1. 结合工厂模式设计,创建一个封装继承的函数
  2. 在函数内部将其实例化,并在对象上做属性扩展并返回

缺点:

  1. 与构造函数继承比较相似,子类只能在函数内部定义方法,由于不能做到复用从而导致低效

class extends

ES5 在继承上,上述的几种方式都有着各自的优劣。所以在实际开发中,都会采用组合编写的方式来互补。

而在 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
24
class Phone {
constructor(brand, price) {
this.brand = brand;
this.price = price;
}

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

class XiaomiPhone extends Phone {
constructor(model, price) {
super('Xiaomi', price);

this.model = model;
}

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

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

泰裤辣!

extends

评论