Class:向传统类模式转变的构造函数

JS基于原型的‘类’,一直被转行前端的码僚们大呼惊奇,但接近传统模式使用 class关键字定义的出现,却使得一些前端同行深感遗憾而纷纷留言:“还我独特的JS”、“净搞些没实质的东西”、“自己没有类还非要往别家的类上靠”,甚至是“已转行”等等。有情绪很正常,毕竟新知识意味着更多时间与精力的开销,又不是简单的闭眼享受。

然而历史的轴印前行依旧,对于 class可以肯定的一点是你不能对面试官说:“拜托,不是小弟不懂,仅仅是不愿意了解,您换个问题呗!”一方面虽然 class只是个语法糖,但 extends对继承的改进还是不错的。另一方面今后可能在‘类’上出现的新特性应该是由 class而不是构造函数承载,谁也不确定它将来会出落得怎样标致。因此,来来来,慢慢的喝下这碗热气腾腾的红糖姜汤。

1 class

ECMAScript中没有类的概念,我们的实例是基于原型由构造函数生成具有动态属性和方法的对象。不过为了与国际接轨,描述的更为简便和高大上,依然会使用‘类’这一词。所以JS的类等同于构造函数。ES6的 class只是个语法糖,其定义生成的对象依然构造函数。不过为了与构造函数模式区分开,我们称其为类模式。学习 class需要有构造函数和原型对象的知识,具体可以自行百度。

Class:向传统类模式转变的构造函数

Class:向传统类模式转变的构造函数

1.1 与变量对比

关键字 class类似定义函数的关键字 function,其定义的方式有声明式和表达式(匿名式和命名式)两种。通过声明式定义的变量的性质与 function不同,更为类似 let和 const,不会提前解析,不存在变量提升,不与全局作用域挂钩和拥有暂时性死区等。 class定义生成的变量就是一个构造函数,也因此,类可以写成立即执行的模式。

Class:向传统类模式转变的构造函数

1.2 与对象对比

类内容( {}里面)的形式与对象字面量相似。不过类内容里面只能定义方法不能定义属性,方法的形式只能是函数简写式,方法间不用也不能用逗号分隔。方法名可以是带括号的表达式,也可以为 Symbol值。方法分为三类,构造方法( constructor方法)、原型方法(存在于构造函数的 prototype属性上)和静态方法(存在于构造函数本身上)

Class:向传统类模式转变的构造函数

不能直接定义属性,并不表示类不能有原型或静态属性。解析 class会形成一个构造函数,因此只需像为构造函数添加属性一样为类添加即可。更为直接也是推荐的是只使用 getter函数定义只读属性。为什么不能直接设置属性?是技术不成熟?是官方希望传递某种思想?抑或仅仅是笔者随意抛出的一个问题?

Class:向传统类模式转变的构造函数

Class:向传统类模式转变的构造函数

1.3 与构造函数对比

下面是使用构造函数和类实现相同功能的代码。直观上, class简化了代码,使得内容更为聚合。 constructor方法体等同构造函数的函数体,如果没有显式定义此方法,一个空的 constructor方法会被默认添加用于返回新的实例。与ES5一样,也可以自定义返回另一个对象而不是新实例。

Class:向传统类模式转变的构造函数

类虽然是个函数,但只能通过 new生成实例而不能直接调用。类内部所定义的全部方法是不可枚举的,在构造函数本身和 prototype上添加的属性和方法是可枚举的。类内部定义的方法默认是严格模式,无需显式声明。以上三点增加了类的严谨性,比较遗憾的是,依然还没有直接定义私有属性和方法的方式。

Class:向传统类模式转变的构造函数

Class:向传统类模式转变的构造函数

Class:向传统类模式转变的构造函数

在方法前加上 static关键字表示此方法为静态方法,它存在于类本身,不能被实例直接访问。静态方法中的 this指向类本身。因为处于不同对象上,静态方法和原型方法可以重名。ES6新增了一个命令 new.target,指代 new后面的构造函数或 class,该命令的使用有某些限制,具体请看下面示例。

Class:向传统类模式转变的构造函数

2 extends

ES5中的经典继承方法是寄生组合式继承,子类会分别继承父类实例和原型上的属性和方法。ES6中的继承本质也是如此,不过实现方式有所改变,具体如下面的代码。可以看到,原型上的继承是使用 extends关键字这一更接近传统语言的形式,实例上的继承是通过调用 super完成子类 this塑造。表面上看,方式更为的统一和简洁。

Class:向传统类模式转变的构造函数

2.1 与构造函数对比

使用 extends继承,不仅仅会将子类的 prototype属性的原型对象( __proto__)设置为父类的 prototype,还会将子类本身的原型对象( __proto__)设置为父类本身。这意味着子类不单单会继承父类的原型数据,也会继承父类本身拥有的静态属性和方法。而ES5的经典继承只会继承父类的原型数据。不单单是财富,连老爸的名气也要获得,不错不错。

Class:向传统类模式转变的构造函数

ES5中的实例继承,是先创造子类的实例对象 this,再通过 call或 apply方法,在 this上添加父类的实例属性和方法。当然也可以选择不继承父类的实例数据。而ES6不同,它的设计使得实例继承更为优秀和严谨。

在ES6的实例继承中,是先调用 super方法创建父类的 this(依旧指向子类)和添加父类的实例数据,再通过子类的构造函数修饰 this,与ES5正好相反。ES6规定在子类的 constructor方法里,在使用到 this之前,必须先调用 super方法得到子类的 this。不调用 super方法,意味着子类得不到 this对象。

Class:向传统类模式转变的构造函数

2.2 super

关键字 super比较奇葩,在不同的环境和使用方式下,它会指代不同的东西(总的说可以指代对象或方法两种)。而且在不显式的指明是作为对象或方法使用时,比如 console.log(super),会直接报错。

作为函数时。 super只能存在于子类的构造方法中,这时它指代父类构造函数。

作为对象时。 super在静态方法中指代父类本身,在构造方法和原型方法中指代父类的 prototype属性。不过通过 super调用父类方法时,方法的 this依旧指向子类。即是说,通过 super调用父类的静态方法时,该方法的 this指向子类本身;调用父类的原型方法时,该方法的 this指向该(子类的)实例。而且通过 super对某属性赋值时,在子类的原型方法里指代该实例,在子类的静态方法里指代子类本身,毕竟直接在子类中通过 super修改父类是很危险的。

很迷糊对吧,疯疯癫癫的,还是结合着代码看吧!

Class:向传统类模式转变的构造函数

2.3 继承原生构造函数

使用构造函数模式,构建继承了原生数据结构(比如 Array)的子类,有许多缺陷的。一方面由上文可知,原始继承是先创建子类 this,再通过父类构造函数进行修饰,因此无法获取到父类的内部属性(隐藏属性)。另一方面,原生构造函数会直接忽略 call或 apply方法传入的 this,导致子类根本无法获取到父类的实例属性和方法。

Class:向传统类模式转变的构造函数

创建类的过程,是先构造一个属于父类却指向子类的 this(绕口),再通过父类和子类的构造函数进行修饰。因此可以规避构造函数的问题,获取到父类的实例属性和方法,包括内部属性。进而真正的创建原生数据结构的子类,从而简单的扩展原生数据类型。另外还可以通过设置 Symbol.species属性,使得衍生对象为原生类而不是自定义子类的实例。

Class:向传统类模式转变的构造函数

需要注意的是继承 Object的子类。ES6改变了 Object构造函数的行为,一旦发现其不是通过 new Object()这种形式调用的,构造函数会忽略传入的参数。由此导致 Object子类无法正常初始化,但这不是个大问题。

Class:向传统类模式转变的构造函数


分享到:


相關文章: