抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)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('"hello world" from', this.brand + '.');
};

var xiaomi = new Phone('xiaomi');
xiaomi.call(); // -> "hello world" from xiaomi

各类变量中,只有 function 函数才有 prototype 属性,我们将其称之为原型。而在原型属性上,又有着 constructor() 方法,这很容易让新学习这门语言的人感到困惑。

1
2
3
function Phone() {}

console.log(Phone.prototype); // -> {constructor: ƒ}

ES6 提供了更接近传统语言的写法,引入了类这个概念,来作为对象的模板。可以通过 class 关键字,来定义类。

基本上,ES6 的 class 可以看作是一个语法糖,它的绝大部分功能,ES5 都可以做到,不过新的写法能让对象原型的写法更加清晰、更像面向对象编程的语法。上面的代码用 ES6 的 class 改写,就是下面这样。

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

call() {
// ES6 模板字符串
console.log(`"hello world" from ${this.brand}.`);
}
}

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

constructor() 方法

这种新的写法,本质上与文章开头的 ES5 的构造函数 Phone 是一致的,ES6 的类,能当做是构造函数的另一种写法。

1
2
3
4
class Phone {}

console.log(typeof Phone); // -> "function"
console.log(Phone === Phone.prototype.constructor); // -> true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。使用的时候,也是直接对类使用 new 关键字,跟构造函数的用法完全一致。

constructor() 方法是类的默认方法,通过 new 关键字生成对象实例时,自动调用该方法。一个类必须有构造方法,如果没有显式定义,一个空的构造方法会被默认添加。

1
2
3
4
5
6
class Phone {}

// 等价于
class Phone {
constructor() {}
}

上面代码中,定义了一个空的 Phone 类,JavaScript 引擎会自动为它添加一个空的 constructor() 方法,该方法默认返回实例对象(this)。

语法糖,但没完全语法糖

不管是百度也好,各大培训机构也好,人们都常说 class 仅仅只是一个语法糖。因为在不使用它的情况下,完全可以用 function 实现同样的效果。

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

调用方式不同

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

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

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

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

严格模式

使用 class 创建的类始终都执行在严格模式下。构造函数、静态方法、原型方法、getter 和 setter,都在严格模式下执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Phone {
constructor() {
var a, x, y;
var r = 10;

// SyntaxError: Strict mode code may not include a with statement
with (Math) {
a = PI * r * r;
x = r * cos(PI);
y = r * sin(PI / 2);
}
}
}

不可枚举

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

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

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

class NewPhone {
replaceBattery() {
console.log('杂鱼❤杂鱼❤~忘了充电就只能等着关机的杂鱼~');
}
}

console.log(OldPhone.prototype); // -> {constructor: ƒ, replaceBattery: ƒ}
console.log(NewPhone.prototype); // -> {constructor: ƒ, replaceBattery: ƒ}
console.log(Object.keys(OldPhone.prototype)); // -> ['replaceBattery']
console.log(Object.keys(NewPhone.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(`"hello world" from ${this.brand}.`);
}
}

const huawei = new Phone('huawei');

huawei.call(); // -> "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(`"hello world" from ${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('"hello world" from', 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('"hello world" from', 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('"hello world" from', 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('"hello world" from', 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. 与构造函数继承比较相似,子类只能在函数内部定义方法,由于不能做到复用从而导致低效

ES6 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(`"hello world" from ${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

评论