在 JavaScript 中,生成实例对象有着 new function 与 new 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 ();
为什么要拿手机来举例子?嗯...随手写的,并没有什么特别深的含义。~~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 ();
可以看到,尽管在写法上有所差异,但上述代码 的表现行为是完全一致的。基本上 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.' ); } } Pen ();
1 2 3 4 5 function Apple ( ) { console .log ('I have a apple, uhh~ wait, where is my pen?' ); } Apple ();
严格模式 使用 class
创建的类,其内部代码会默认启用严格模式。构造函数、静态方法、原型方法、getter 和 setter,都是在严格模式下强制执行的,无法手动关闭。
1 2 3 4 5 6 7 function Dumbphone ( ) { with (console ) { log ('I am Dumbphone.' ); } } new Dumbphone ();
1 2 3 4 5 6 7 8 class Smartphone { constructor ( ) { with (console ) { log ('I am Smartphone.' ); } } }
不可枚举 class
将 prototype
中的所有方法的 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 ); console .log (Smartphone .prototype ); console .log (Object .keys (Dumbphone .prototype )); 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 (); huawei.hasOwnProperty ('brand' ); huawei.hasOwnProperty ('call' ); huawei.__proto__ .hasOwnProperty ('call' );
上面代码中,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__ );
上面代码中,xiaomi
和 huawei
都是 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 (); huawei.camera (); const nokia = new Phone ('nokia' );nokia.camera ();
上面代码在 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 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 );
思路:
让父类构造函数在子类内部 运行
将父类内部的 this 指向子类的实例
缺点:
父类必须在构造函数中 定义方法
子类不能访问父类原型上 定义的属性
原型继承(经典继承) 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 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 );
思路:
结合工厂模式设计,创建一个封装继承的函数
在函数内部将其实例化 ,并在对象上做属性扩展并返回
缺点:
与构造函数继承比较相似,子类只能在函数内部定义方法,由于不能做到复用从而导致低效
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 );
泰裤辣!