第9章 Classes

Classes

我们知道javascript是没有类的概念的, ES6新添加了 class 关键词,为书写类提供了很大的便利,下面我们通过ES5和ES6对比来看一下ES6这种新的语法糖。

  1. class 与 function ;
  2. class 与 对象字面量 ;
  3. 静态方法 "static" 关键词 ;
  4. 继承 "extends", "super" ;
  5. 抽象类 new.target

一. class和函数的关系

1.ES5类的表达方式 和 ES6 class

ES5通过函数构造器和原型的方法来模拟类的概念

function Person(name, age) {
    this.name = name;
    this.age = age;
}
// 将方法写在原型链上的原因是:函数是对象
// 如果每次实例化一个对象都创建一个相同的函数,这样会占用很多内存
Person.prototype.sayName = function() {
    console.log(this.name);
}
// 实例化一个对象
var person = new Person("james", 26);
person.sayName(); // "james"
console.log(person instanceof Person); // true 

ES6 通过 class关键词, 还有constructor()方法, 将实例属性写在构造器函数中, 原型上的属性使用简写语法,上面的例子相当于:

class Person {
    // 构造器
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    // 简写语法,原型链上的属性
    sayName() {
        console.log(this.name);
    }
}
// 实例化 new操作符
var person = new Person("kobe", 37);
person.sayName(); // "kobe"
// 注意下面Person的实质类型
console.log(typeof Person); // "function"
console.log(typeof Person.prototype.sayName); // "function"

2.class语法和自定义函数表示类的区别

  1. class declarations 不提升(hoisting),就像用let声明一样,函数声明会自动提升;
  2. 在class declarations中,所有的代码自动在严格模式下;
  3. class中所有的方法都是不可枚举的(nonenumerable).函数声明要想方法不可枚举,需要使用Object.defineProperty方法;
  4. class中的方法内部都不存在[[Construct]]属性,因此不能使用new;
  5. 调用class构造器必须使用new,否则抛出异常;
  6. 尝试用class中的方法改写 class name会抛出错误,在外部改写,则像使用let声明之后重新赋值一样,不会抛出错误;

class语法相当于下面代码:

// 使用let声明,避免变量提升
let PersonType = (function() {
    "use strict";
        
    // 使用const, 内部不能改变class 名称
     const PersonType = function(name) {
         // 确保构造器使用new
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new");
        }
        this.name = name;
    }
    // 方法不可枚举
    Object.defineProperty(PersonType.prototype, "sayName", {
        value: function() { 
            // 确保方法不使用new操作符
            if (typeof new.target !== "undefined") {
                console.log(this.name)
            }     
        },
        enumerable: false,
        writable: true,
        configurable: true
    });

    return PersonType;    
}());

3.Class Expressions

class 和 function 的相似之处都有两种形式: 声明式 和 表达式, class表达式一般用作变量声明或者当作参数传入函数中。
表达式形式:

let Person = class {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
};

功能上与class声明形式一样,就是"name"属性不一样(因为class本质上是function,ES6函数新添加了name 属性)。上面的匿名class表达式 Person.name 为空字符串""

4.Named Class expressions

这个和Named Function expressions一样,就是在class后面添加一个标识符, 该标识符只能在class定义内部使用

let Person = class PersonType {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}

console.log(typeof Person); // "function"
console.log(typeof PersonType); // undefined PersonType只能在class内部使用

相当于:

   let Person = (function() {
    "use strict";
        
    // 使用const, 内部不能改变class 名称 此处和 Person不一样
     const PersonType = function(name) {
         // 确保构造器使用new
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new");
        }
        this.name = name;
    }
    // 方法不可枚举
    Object.defineProperty(PersonType.prototype, "sayName", {
        value: function() { 
            // 确保方法不适用new操作符
            if (typeof new.target !== "undefined") {
                console.log(this.name)
            }     
        },
        enumerable: false,
        writable: true,
        configurable: true
    });

    return PersonType;    
}());

二.class和对象字面量关系

1.访问器get,set属性

class能够在构造器中定义实例属性,也可以在原型上定义访问器属性(accessor properties), 定义方法和在对象字面量中的方式一致:

class CustomHTMLElement {
    constructor(element) {
        this.element = element;
    }
    get html() {
        return this.element.innerHTML;
    }
    set html(value) {
        this.element.innerHTML = value;
    }
}

var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false

熟悉jquery的应该知道html()方法,原理和这个差不多,相当于:

let CustomHTMLElement = (function() {
    function CustomHTMLElement(element) {
        // 确保使用new调用构造器
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new");
        }
        this.element = element;
    }
    Object.defineProperty(CustomHTMLElement.prototype, "html", {
        enumerable: false,
        configurable: true,
        get: function() {
            return this.element.innerHTML;
        },
        set: function(value) {
            this.element.innerHTML = value;
        }
    });
    return CustomHTMLElement;
}());

2.Computed Member Names

对象字面量中有个计算变量属性,ES6新添加的语法, 同样class 方法和访问器 也可以采用类似的计算命名

let methodName = "sayName";
class Person {
    constructor(name) {
        this.name = name;
    }
    [methodName]() {
        console.log(this.name);
    }
}

访问器:

let propertyName = "html";
class CustomHTMLElement {
    constructor(element) {
        this.element = element;
    }
    get [propertyName]() {
        return this.element.innerHTML;
    }
    set [propertyName](value) {
        this.element.innerHTML = value;
    }    
}

3.产生器方法(generator method)

在方法名前添加 "*",可以将任意方法变为一个产生器, Generator method对对象是集合类型值,进行迭代十分的方便, 定义一个默认的迭代器
可以通过 Symbol.iterator 来定义一个默认产生器方法

class Collection {
    constructor() {
        // 属性为集合类型
        this.items = [];
    }
    // 定义一个默认的产生器方法, 使用computed name
    *[Symbol.iterator]() {
        // 利用数组默认的迭代器values()
        yield *this.items.values();
    }
}

var c = new Collection();
c.items.push(1);
c.items.push(2);
c.items.push(3);
for (let n of c) {
    console.log(n);
}
// 1
// 2
// 3

// 使用spread操作符
var arr = [...c];
arr; // [1, 2, 3]

4.first-citizen

class的本质是function,意味着可以当作对象一样当作参数或者作为返回值。

1.当作参数

function createObj(classDef) {
    return new classDef();
}
let obj = createObj(class {
    sayHi() {
        console.log("hi");
    }
});
obj.sayHi(); // "hi"

2.创建单利(singletons),利用IIEF

let person = new class {
    constructor(name) {
        this.name = name;
    }

    sayName() {
        console.log(this.name);
    }
}("Nicholas");

person.sayName(); // "Nicholas"

三.静态方法 static

ES6之前可以通过直接添加方法到构造器来模拟静态方法(区别于实例方法)

function PersonType(name) {
    this.name = name;
}

// 模拟静态方法 static method
PersonType.create = function(name) {
    // 调用构造器    
    return new PersonType(name);
}

// instance method
PersonType.prototype.sayName = function() {
    console.log(this.name);
}

// 直接对类PersonType调用方法
var person = PersonType.create("Mike"); 

ES6 引入 static 关键词

class PersonType {
    constructor(name) {
        this.name = name;
    }
    // instance method
    sayName() {
        console.log(this.name);
    }

    // static method
    static create(name) {
        return new PersonType(name);
    }
}

我们可以对任何方法添加 static 关键词 使之变为静态方法,唯一不能对constructor构造器添加static; 另外,静态成员只能直接通过class,而不能使用实例去访问


四.继承

ES6之前,实现继承是比较繁琐的步骤,我们需要使用构造器窃取,将子类指向一个父类的实例(或者使用Object.create()方法),下面我们来看一下

1.ES6之前模拟类继承

function Rectangle(width, height) {
    this.width = width;
    this.height = height;
}
Rectangle.prototype.getArea = function() {
    return this.width * this.height;
}

function Square(width) {
    // 构造器窃取
    Rectangle.call(this, width, width);
}
// 实现继承
Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        // 将原型重新指向自身
        value: Square,
        enumerable: true,
        wriable: true,
        configurable: true
    }
});
var square = new Square(5);
square.getArea(); // 25
square instanceof Square; // true
square instanceof Rectangle; // true

2.ES6通过 super() 和 extends 实现继承

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    getArea() {
        return this.width * this.height;
    }
}

class Square extends Rectangle {
    constructor(width) {
        // 使用super()
        super(width, width);
    }
}

var square = new Square(5);
square.getArea(); // 25
square instanceof Square; // true
square instanceof Rectangle; // true

可以看出这种写法十分的简洁,同时也十分的清晰

使用super()注意事项:

  1. 只能在派生类(即子类)构造器中使用super(),在非派生类中或者一个function中使用super,会抛出异常;
  2. 在访问this之前必须先调用super(),因为super()是负责实例化this的;
  3. 唯一能避免调用super()的方式是从一个对象构造器中返回一个对象

3.子类override父类方法

学过其他编程语言的肯定知道override,可以用来重写父类中的属性,ES6也提供了这样的选择,可以直接重写,也可以使用super.property方式

class Square extends Rectangle {
    constructor(width) {
        super(width, width);
    }

    // 重写或者shadow父类原型上的方法getArea
    getArea() {
        return this.width * this.width;
    }
} 

或使用super

class Square extends Rectangle {
    constructor(width) {
        super(width, width);
    }
    // override or shadow 并调用父类原型上的方法
    getArea() {
        return super.getArea();
    }
}

4.继承父类静态属性

父类上的静态属性同样可以被继承,这是ES6新概念

 class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    getArea() {
        return this.width * this.height;
    }
    // 静态方法
    static create(width, height) {
        return new Rectangle(width, height);
    } 
}

class Square extends Rectangle {
    constructor(width) {
        super(width, width);
    }
}

// 子类调用父类静态方法
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
rect.getArea(); // 12
console.log(rect instanceof Square); // false 注意!

5.派生类继承自表达式

1.当一个表达式拥有[[Construct]]属性 和 原型 就能够被其他类继承,这种方式给动态的继承提供了很大的便利,后面会谈到
比如:

// 构造器, 拥有[[Construct]]属性
function Rectangel(width, height) {
    this.width = width;
    this.height = height;
}
// 拥有原型
Rectangle.prototype.getArea = function() {
    return this.widht * this,height;
}

// 实现继承
class Square extends Rectangle {
    constructor(width) {
        super(width, width);
    }
}
var x = new Square(4);
x.getArea(); // 16
x instanceof Rectangle; // true

2.动态的决定继承自谁

// 构造器, 拥有[[Construct]]属性
function Rectangel(width, height) {
    this.width = width;
    this.height = height;
}
// 拥有原型
Rectangle.prototype.getArea = function() {
    return this.widht * this,height;
}

// 利用这个函数,返回函数对象Rectangle    
function getBase() {
    return Rectangle;
}
// 动态实现继承, 调用getBase()
class Square extends getBase() {
    constructor(width) {
        super(width, width);
    }
}
var x = new Square(4);
x.getArea(); // 16
x instanceof Rectangle; // true

3.上面的方法可以用来创建mixins, 将多个属性添加到同一个类中,然后子类继承该mixins

let serializableMixin = {
    serialize() {
        return JSON.stringify(this);
    }
};

let AreaMixin = {
    getArea() {
        return this.width * this.height;
    }
};

// 创建mixins,利用Object.assign()
function Mixins(...mixins) {
    // 创建一个空的base函数
    var base = function() {};
    Object.assign(base.prototype, ...mixins);
    return base;
}
// 继承Mixins
class Square extends Mixins(serializableMixin, AreaMixin) {
    constructor(width) {
        super(width, width);
        // 两个实例属性
        this.width = width;
        this.height = width;
    }
}    
var s = new Square(3);
s.getArea(); // 9
s.serilize(); // {"width": 3, "height: 3}

4.继承内置类(built-in)
ES6之前对继承内置类会出现问题,但是ES6允许继承内置类
比如继承Array类,则拥有Array上的属性:

class MyArray extends Array {
    // empty
}
var myArray = new MyArray();
myArray[0] = "red";
myArray.length; // 1

5.Symbol.species

Symbol.species: 一个静态访问器属性,用于返回一个function,这个function是一个构造器,当实例必须使用内部方法创建时,下列内置类型拥有 Symbol.species 属性:

  1. Array
  2. ArrayBuffer
  3. Map
  4. Promise
  5. RegExp
  6. Set
  7. TypeArray

eg:

class MyClass {
    static get [Symbol.species]() {
        return this;
    }

    constructor(value) {
        this.value = value;
    }

    // 使用内置方法创建实例, 允许派生类改写这个值
    clone() {
        return new this.constructor[Symbol.species](this.value);
    }
}

// 派生类
class DerivedClass1 extends MyClass {
    // empty
}
class DerivedClass2 extends MyClass {
    // 改写
    static get [Symbol.species]() {
        return MyClass;
    }
}
let instance1 = new DerivedClass1("foo"),
    clone1 = instance1.clone();
let instance2 = new DerivedClass2("bar"),
    clone2 = instance2.clone();

instance1 intanceof MyClass; // true
clone1 instanceof DerivedClass1; // true
instance2 instanceof MyClass; // true
clone2 instanceof DerivedClass2; // false

比如上面的例子:

class MyArray extends Array {
      static get [Symbol.species]() {
        return Array;
      }
}
var arr = new MyArray(1,2,3,4);
var sub = arr.slice(1, 3);
sub instanceof Array; // true
sub instanceof MyArray; // false

五.创建抽象类

抽象类的本质就是当父类不能实例化,当父类使用new操作符时抛出异常,这我们可以相当使用new.target来控制

class Sharp {
    contructor(width, height) {
        // 如果new 的目标是 类 本身 则抛出异常
        if (new.target === Sharp) {
            throw new Error("Abstract class cannot be instantiated");
        }

    }
}

class Rectangle extends Sharp {
    contructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }
}

var sharp = new Sharp(); // ERROR
var rec = new Rectangel(10, 5);
rec instanceof Sharp; // true

总结

class的出现为类的创建提供了极大的便利,这种语法糖其本质还是function,拥有函数该有的特性,比如几种表达方式,作为first-citizen,能够作为参数或者返回值;另外书写方面和对象字面量又十分的相识,提供各种简写。另外最重要的是对实现继承是非常的友好,同时mixins,对扩展类的功能提供了很大方便,还有抽象类的出现,这使得javascript更加的OOP了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容