var p = new Point(1,4);
console.log(p+“”); //(1,4)
console.log(typeof Point); //“function”
console.log(Point.prototype.constructor === Point); //true
console.log(Point.prototype.constructor === p.constructor); //true
Point.show(); //“Static function!”
相当于传统写法:
function Point(x, y){
this.x = x;
this.y = y;
}
Point.prototype.toString = function(){
return (${this.x},${this.y})
;
}
Point.show = function(){
console.log(“Static function!”);
}
var p = new Point(1,4);
console.log(p+“”); //(1,4)
这里不难看出,class 的类名就是 ES5 中的构造函数名,静态方法就定义在其上,而类的本质依然是个函数。而 class 中除了 constructor 是定义的构造函数以外,其他的方法都定义在类的 prototype 上,这都和 ES5 是一致的,这就意味着,ES5 中原有的那些方法都可以用, 包括但不限于:
-
Object.keys()
,Object.assign()
等等 -
而且 class 也同样支持表达式做属性名,比如 Symbol
-
ES5 函数具有的属性/方法:length、name、apply、call、bind、arguments 等等
但有些细节还是有区别的,比如:
class Point{
constructor(x, y){ //定义构造函数
this.x = x; //定义属性x
this.y = y; //定义属性y
} //这里没有逗号
toString(){ //定义动态方法,不需要 function 关键字
return (${this.x},${this.y})
;
}
getX(){
return this.x;
}
getY(){
return this.y;
}
}
var p = new Point(1,4);
var keys = Object.keys(Point.prototype);
var ownKeys = Object.getOwnPropertyNames(Point.prototype);
console.log(keys); //[]
console.log(ownKeys); //[“constructor”, “toString”, “getX”, “getY”]
console.log(p.hasOwnProperty(“toString”)); //false
console.log(p.proto.hasOwnProperty(“toString”)); //true
//ES5
function Point(x, y){
this.x = x;
this.y = y;
}
Point.prototype = {
toString(){
return (${this.x},${this.y})
;
},
getX(){
return this.x;
},
getY(){
return this.y;
}
}
var p = new Point(1,4);
var keys = Object.keys(Point.prototype);
var ownKeys = Object.getOwnPropertyNames(Point.prototype);
console.log(keys); //[“toString”, “getX”, “getY”]
console.log(ownKeys); //[“toString”, “getX”, “getY”]
console.log(p.hasOwnProperty(“toString”)); //false
console.log(p.proto.hasOwnProperty(“toString”)); //true
这个例子说明,class 中定义的动态方法是不可枚举的,并且 constructor 也是其自有方法中的一个。
使用 class 注意一下几点:
-
class 中默认是严格模式,即使不写
"use strict
。关于严格模式可以看:Javascript基础(2) - 严格模式特点 -
同名 class 不可重复声明
-
class 相当于 object 而不是 map,不具有 map 属性,也不具有默认的 Iterator。
-
constructor 方法在 class 中是必须的,如果没有认为指定,系统会默认生成一个空的 constructor
-
调用 class 定义的类必须有 new 关键字,像普通函数那样调用会报错。ES5 不限制这一点。
TypeError: Class constructor Point cannot be invoked without ‘new’
- constructor 方法默认返回值为 this,可以认为修改返回其他的值,但这会导致一系列奇怪的问题:
class Point{
constructor(x,y){
return [x, y];
}
}
new Point() instanceof Point; //false
- class 声明类不存在变量提升
new Point(); //ReferenceError: Point is not defined
class Point{}
class 表达式
这个和面向对象不一样了,js 中函数可以有函数声明形式和函数表达式2种方式定义,那么 class 一样有第二种2种定义方式:class 表达式
var className1 = class innerName{
//…
};
let className2 = class innerName{
//…
};
const className3 = class innerName{
//…
};
class 表达式由很多特性和 ES5 一样:
-
和函数表达式类似,这里的innerName可以省略,而且innerName只有类内部可见,实际的类名是赋值号前面的 className。
-
这样定义的类的作用域,由其所在位置和声明关键字(var, let, const)决定
-
const申明的类是个常量,不能修改。
-
其变量声明存在提升,但初始化不提升
-
class 表达式也不能和 class 申明重名
ES5 中有立即执行函数,类似的,这里也有立即执行类:
var p = new class {
constructor(x, y){
this.x = x;
this.y = y;
}
toString(){
return (${this.x},${this.y})
;
}
}(1,5); //立即生成一个对象
console.log(p+“”); //(1,5)
getter, setter 和 Generator 方法
getter 和 setter 使用方式和 ES5 一样, 这里不多说了,举个例子一看就懂:
class Person{
constructor(name, age, tel){
this.name = name;
this.age = age;
this.tel = tel;
this._self = {};
}
get id(){
return this._self.id;
}
set id(str){
if(this._self.id){
throw new TypeError(“Id is read-only”);
} else {
this._self.id = str;
}
}
}
var p = new Person(“Bob”, 18, “13211223344”);
console.log(p.id); //undefined
p.id = ‘30010219900101009X’;
console.log(p.id); //‘30010219900101009X’
var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, ‘id’);
console.log(‘set’ in descriptor); //true
console.log(‘get’ in descriptor); //true
p.id = ‘110’; //TypeError: Id is read-only
Generator 用法也和 ES6 Generator 部分一样:
class Person{
constructor(name, age, tel){
this.name = name;
this.age = age;
this.tel = tel;
this._self = {};
}
var keys = Object.keys(this);
keys = keys.filter(function(item){
if(/^_/.test(item)) return false;
else return true;
});
for(let item of keys){
yield this[item];
}
}
get id(){
return this._self.id;
}
set id(str){
if(this._self.id){
throw new TypeError(“Id is read-only”);
} else {
this._self.id = str;
}
}
}
var p = new Person(“Bob”, 18, “13211223344”);
p.id = ‘30010219900101009X’;
for(let info of p){
console.log(info); //依次输出: “Bob”, 18, “13211223344”
}
class 的继承
这里我们只重点讲继承,关于多态没有新的修改,和 ES5 中一样,在函数内判断参数即可。关于多态可以阅读Javascript对象(1) - 对象、类与原型链中关于多态重构
的部分。
此外,class 继承属于 ES5 中多种继承方式的共享原型,关于共享原型也在上面这篇文章中讲解过。
class 实现继承可以简单的通过 extends 关键字实现, 而使用 super 关键字调用父类方法:
//定义 ‘有色点’’ 继承自 ‘点’
class ColorPoint extends Point{ //这里延用了上面定义的 Point 类
constructor(x, y, color){
super(x, y); //利用 super 函数调用父类的构造函数
this.color = color;
}
toString(){
return ${super.toString()},${this.color}
; //利用 super 调用父类的动态方法
}
}
var cp = new ColorPoint(1, 5, ‘#ff0000’);
console.log(cp+“”); //(1,5),#ff0000
ColorPoint.show(); //“Static function!” 静态方法同样被继承了
cp instanceof ColorPoint; //true
cp instanceof Point; //true
使用 extends 继承的时候需要注意一下几点:
-
super 不能单独使用,不能访问父类属性,只能方法父类方法和构造函数(super本身)
-
子类没有自己的 this,需要借助 super 调用父类构造函数后加工得到从父类得到的 this,子类构造函数必须调用 super 函数。这一点和 ES5 完全不同。
-
子类如果没有手动定义构造函数,会自动生成一个构造函数,如下:
constructor(…args){
super(…args);
}
-
子类中使用 this 关键字之前,必须先调用 super 构造函数
-
由于继承属于共享原型的方式,所以不要在实例对象上修改原型(
Object.setPrototypeOf
,obj.__proto__
等) -
super 也可以用在普通是对象字面量中:
var obj = {
toString(){
return MyObj ${super.toString()}
;
}
}
console.log(obj+“”); //MyObj [object Object]
prototype
和 __proto__
在 class 的继承中
-
子类的
__proto__
指向其父类 -
子类 prototype 的
__proto__
指向其父类的 prototype
class Point{
constructor(x, y){
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point{
constructor(x, y, color){
super(x, y);
this.color = color;
}
}
ColorPoint.proto === Point; //true
ColorPoint.prototype.proto === Point.prototype; //true
其等价的 ES5 是这样的:
function Point(){
this.x = x;
this.y = y;
}
function ColorPoint(){
this.x = x;
this.y = y;
this.color = color;
}
Object.setPrototypeOf(ColorPoint.prototype, Point.prototype); //继承动态方法属性
Object.setPrototypeOf(ColorPoint, Point); //继承静态方法属性
ColorPoint.proto === Point; //true
ColorPoint.prototype.proto === Point.prototype; //true
这里我们应该理解一下3种继承的 prototype 和 __proto__
:
- 没有继承
class A{}
A.proto === Function.prototype; //true
A.prototype.proto === Object.prototype; //true
- 继承自 Object
class A extends Object{}
A.proto === Object; //true
A.prototype.proto === Object.prototype; //true
- 继承自 null
class A extends null{}
A.proto === Function.prototype; //true
A.prototype.proto === undefined; //true
判断类的继承关系:
class A{}
class B extends A{}
Object.getPrototypeOf(B) === A; //true
子类的实例的 __proto__
的 __proto__
指向其父类实例的 __proto__
class A{}
class B extends A{}
var a = new A();
var b = new B();
B.proto.proto === A.proto; //true
因此,可以通过修改子类实例的 __proto__.__proto__
改变父类实例的行为。建议:
-
总是用 class 取代需要 prototype 的操作。因为 class 的写法更简洁,更易于理解。
-
使用 extends 实现继承,因为这样更简单,不会有破坏 instanceof 运算的危险。
此外存取器和 Generator 函数都可以很理想的被继承:
class Person{
constructor(name, age, tel){
this.name = name;
this.age = age;
this.tel = tel;
this._self = {};
}
var keys = Object.keys(this);
keys = keys.filter(function(item){
if(/^_/.test(item)) return false;
else return true;
});
for(let item of keys){
yield this[item];
}
}
get id(){
return this._self.id;
}
set id(str){
if(this._self.id){
throw new TypeError(“Id is read-only”);
} else {
this._self.id = str;
}
}
}
class Coder extends Person{
constructor(name, age, tel, lang){
super(name, age, tel);
this.lang = lang;
}
}
var c = new Coder(“Bob”, 18, “13211223344”, “javascript”);
c.id = ‘30010219900101009X’;
for(let info of c){
console.log(info); //依次输出: “Bob”, 18, “13211223344”, “javascript”
}
console.log(c.id); //‘30010219900101009X’
c.id = “110”; //TypeError: Id is read-only
多继承
多继承指的是一个新的类继承自已有的多个类,JavaScript 没有提供多继承的方式,所以我们使用 Mixin 模式手动实现:
function mix(…mixins){
class Mix{}
for(let mixin of mixins){
copyProperties(Mix, mixin); //继承静态方法属性
copyProperties(Mix.prototype, mixin.prototype); //继承动态方法属性
}
return Mix;
function copyProperties(target, source){
for(let key of Reflect.ownKeys(source)){
if(key !== ‘constructor’ && key !== “prototype” && key !== “name”){
if(Object(source[key]) === source[key]){
target[key] = {};
copyProperties(target[key], source[key]); //递归实现深拷贝
} else {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
}
}
//使用方法:
class MultiClass extends mix(superClass1, superClass2, /…/){
//…
}
由于 mixin 模式使用了拷贝构造,构造出的子类的父类是 mix 函数返回的 class, 因此 prototype 和 __proto__
与任一 superClass 都没有直接的联系,instanceof 判断其属于 mix 函数返回类的实例,同样和任一 superClass 都没有关系。可以这么说:我们为了实现功能破坏了理论应该具有的原型链。
原生构造函数继承
在 ES5 中,原生构造函数是不能继承的,包括: Boolean(), Number(), Date(), String(), Object(), Error(), Function(), RegExp()等,比如我们这样实现:
function SubArray(){}
Object.setPrototypeOf(SubArray.prototype, Array.prototype); //继承动态方法
Object.setPrototypeOf(SubArray, Array); //继承静态方法
var arr = new SubArray();
arr.push(5);
arr[1] = 10;
console.log(arr.length); //1 应该是2
arr.length = 0;
console.log(arr); //[0:5,1:10] 应该为空
很明显这已经不是那个我们熟悉的数组了!我们可以用 class 试试:
class SubArray extends Array{}
var arr = new SubArray();
arr.push(5);
arr[1] = 10;
console.log(arr.length); //2
arr.length = 0;
console.log(arr); //[]