在 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 ();
为什么要拿手机来举例子?嗯……随手写的,并没有什么特别深的含义。
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 ();
可以看到,尽管在写法上有所差异,但上述代码的表现行为是完全一致的。我们可以将 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?' ); } } Smartphone ();Smartphone .crackWalnuts ();
1 2 3 4 5 6 7 8 function Dumbphone ( ) {}Dumbphone .crackWalnuts = function ( ) { console .log ('Crack!' ); }; Dumbphone ();Dumbphone .crackWalnuts ();
可以看到,尽管 Dumbphone 添加了静态方法,但它依然可以作为函数正常执行,不受任何影响。
严格模式 使用 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 会将其原型上的方法的 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 ); console .log (Smartphone .prototype ); console .log (Object .keys (Dumbphone .prototype )); console .log (Object .keys (Smartphone .prototype )); console .log (Object .getOwnPropertyNames (Smartphone .prototype ));
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 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 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 );
泰裤辣!