九: ES6 Class 类

前言

该部分为书籍 深入理解ES6 第九章(JS的类)笔记

ES5 中的仿类结构

在 ES5 中与类最接近的是: 创建一个构造器, 然后将方法指派到该构造器的原型上, 这种方式通常被称为创建一个自定义类型

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

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

let person = new PersonType("Nicholas");
person.sayName(); // 输出 "Nicholas"

console.log(person instanceof PersonType); // true
console.log(person instanceof Object); // true

类的声明

类在 ES6 中最简单的形式就是类声明, 看起来"很像"其他语言中的类

1. 基本的类声明

class 关键字开始, 其后是类的名称; 剩余部分的语法看起来就像对象字面量中的方法简写, 在方法之间不需要使用逗号.

class PersonClass {
    // 等价于 PersonType 构造器
    constructor(name) {
        this.name = name;
    }
    
    // 等价于 PersonType.prototype.sayName
    sayName() {
        console.log(this.name)
    }
}

let person = new PersonClass('Nicholas');
person.sayName(); // 输出 "Nicholas"

console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
// class 类也属于函数
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

2. 与自定义类型的区别

尽管类与自定义类型之间有相似性, 但仍然有一些重要的区别:

  1. 类声明不会被提升, 这与函数定义不同. 类声明的行为与 let 相似, 因此在程序的执行到达声明处之前, 类会存在与暂时性死区内.
  2. 类声明中的所有代码会自动运行在严格模式下, 并且也无法退出严格模式
  3. 类的所有方法都是不可枚举的, 这是对与自定义类型的显著变化, 后者必须用 Object.defineProperty() 才能将方法改变为不可枚举的.
  4. 类的所有方法内部都没有 [[Construct]], 因此使用 new 来调用它们会抛出错误.
  5. 调用类构造器时不使用 new , 会抛出错误
  6. 试图在类的方法内部重写类名, 会抛出错误

上例中 PersonClass 类声明实际上就直接的等价于以下未使用类语法的代码:

// 直接等价与 PersonClass
let PersonType2 = (function() {
   "use strict";
    
    // 这里确保了第 6 点: 不能在类的内部重写类名
    const PersonType2 = function(name) {
        // 确认函数被调用时使用了 new
        if (typeof new.target === 'undefind') {
            throw new Error("Constructor must be called with new.");
        }
        
        this.name = name;
    }
    
    Object.defineProperty(PersonType2.prototype, "sayName" {
        value: function() {
            // 确认函数被调用时没有使用 new
            if (typeof new.target === 'undefind') {
                throw new Error("Method cannot be called with new.")
            }
            
            console.log(this.name);
        },
        enumerable: false,
        writable: true,
        configurable: true
    });
    
    return PersonType2;
}())

只有在类的内部, 类名才被视为是使用 const 声明的. 也就意味着可以在外部重写类名, 但不能在类的方法内部这么做

class Foo {
  constructor() {
      Foo = "bar"; // 执行时抛出错误
  }
}

// 但在类声明之后没问题
Foo = "baz";

类表达式

类与函数有相似之处, 即它们都有两种形式: 声明与表达式. 类表达式被设计用于变量声明, 或可作为参数传递给函数

1. 基本的类表达式

let PersonClass = class {
    // 等价于 PersonType 构造器
    constructor(name) {
        this.name = name;
    }
    
    // 等价于 PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
};

let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"

console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

除了语法差异, 类表达式的功能等价于类声明. 相对于函数声明与函数表达式之间的区别, 类声明与类表达式都不会被提升, 因此对代码运行时的行为影响甚微

2. 具名类表达式

上面的示例中使用的是一个匿名的类表达式, 不过就像函数表达式那样, 也可以为类表达式命名. 为此需要在 class 关键字后添加标识符

let PersonClass = class PersonClass2 {
    // 等价于 PersonType 构造器
    constructor(name) {
        this.name = name;
    }
    
    // 等价于 PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
}

console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass2); // "undefined"

类标识符 PersonClass2 只在类定义内部中存在, 只能用在类方法内部

作为一级公民的类

在编程中, 能被作为值值来使用的就成为一级公民(first-class citizen), 意味着它能作为参数传给函数、能作为函数返回值、能用来给变量赋值。 JS的函数就是一级公民

类同样也是一级公民

  1. 作为参数传入函数:

    function createObject(classDef) {
     sreturn new classDef();
    }
    let obj = createObject(class {
     sayHi() {
         console.log("Hi!");
     }
    });
    obj.sayHi(); // "Hi!"
    
  2. 类表达式的另一个有趣用途是立即调用类构造器, 以创建单例

    let person = new class {
        construstor(name) {
            this.name = name;
        }
        
        sayName() {
            console.log(this.name);
        }
    }("Nicholas");
    
    person.sayName(); // "Nicholas"
    

访问器属性

创建一个 getter, 使用 get 关键字, 并要与后方标识符之间留出空格;

创建一个 setter, 使用 set 关键字, 并要与后方标识符之间留出空格;

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

需计算的成员名

对象字面量与类之间的相似点还不仅前面那些. 类方法与类访问器属性也都能使用需计算的名称. 语法相同于对象字面量中的需计算名称: 无须使用标识符, 而是用方括号来包裹一个表达式

let methodName = "sayName",
    propertyName = "html";

class PersonClass {
    constructor(name) {
        this.name = name;
    }
    
    [methodNmae]() {
        console.log(this.name);
    }
    
    set [propertyName](value) {
        this.name = value;
    }
    
    get [propertyName]() {
        return this.name;
    }
}

let me = new PersonClass("Nicholas");
me.sayName(); // "Nicholas"

生成器方法

在类上同样可以定义一个生成器

class MyClass {
    *createIterator() {
        yield 1;
        yield 2;
        yield 3;
    }
}

let instance = new MyClass();
let iterator = instance.createIterator();

**也可以自定义一个默认迭代器, 让实例成为可迭代对象 **

class Collection {
    constructor() {
        this.items = [];
    }
    *[Symbol.iterator]() {
        yield *this.items.values();
    }
}

var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
    console.log(x);
}

// 输出:
// 1
// 2
// 3

静态成员

静态成员表示为定义在类上面的方法(类 相当于 函数, 也可以当作对象一样添加属性和方法) ==> 静态成员不能用实例来访问, 始终需要直接用类自身来访问它们

类定义静态成员, 只要在方法与访问器属性的名称前添加正式的 static 标注

注意: 可以在类中的任何方法与访问器属性上使用 static 关键字, 唯一限制就是不能将它用于 constructor 方法的定义

class PersonClass {
    // 等价于 PersonType 构造器
    constructor(name) {
        this.name = name;
    }
    
    // 等价于 PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
    
    // 等价于 PersonType.create
    static create(name) {
        return new PersonClass(name);
    }
}

let person = PersonClass.create("Nicholas");

使用派生类进行继承

类继承使用 extends 关键字来指定当前类所需要继承的函数, 生成类的原型会被自动调整, 还能调用 super() 方法来访问基类的构造器

// ES5的继承
function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

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

function Square(length) {
    Rectangle.call(this, length, length);
}

Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        value: Square,
        enumerable: true,
        writable: true,
        configurable: true
    }
})

var square = new Square(3);

console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true

// ES6的继承
class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    
    getArea() {
        return this.length * this.width;
    }
}

class Square extends Rectangle {
    construstor(length) {
        // 与 Rectangle.call(this, length, length)相同
        super(length, length);
    }
}

var square = new Square(3);

console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true

继承了其他类的类被称为派生类. 如果派生类指定了构造器, 就需要使用 super(), 否则会造成错误. 若选择不使用构造器, super() 方法会被自动调用, 并会使用创建新实例时提供的所有参数

class Square extends Rectangle {
    // 没有构造器
}

// 等价于:
class Square extends Rectangle {
    constructor(...args) {
        super(...args);
    }
}

使用 super() 时注意点:

  1. 只能在派生类中使用 super(). 若尝试在非派生的类( 即: 没有使用 extends 关键字的类)或函数中使用它, 就会抛出错误.
  2. 在构造器中, 你必须在访问 this 之前调用 super(). 由于 super() 负责初始化 this, 因此试图先访问 this 自然就会造成错误.
  3. 唯一能避免调用 super() 的方法, 是从类构造器中返回一个对象.

1. 屏蔽类方法

派生类中的方法总是会屏蔽基类的同名方法

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }
    
    // 重写病屏蔽 Rectangle.prototype.getArea()
    getArea() {
        return this.length * this.length;
    }
}

当然, 也可以使用 super.getArea() 方法来调用基类中的同名方法

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }
    
    // 重写、屏蔽并调用了 Rectangle.prototype.getArea()
    getArea() {
        // 效果等同于对象中 super 引用(即引用的是当前原型对象), 并且 this 值会被自动设置为当前实例
        return super.getArea();
    }
}

2. 继承静态成员

如果基类包好静态成员, 那么这些静态成员在派生类中会自动继承

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    getArea() {
        return this.length * this.width;
    }
    static create(length, width) {
        return new Rectangle(length, width);
    }
}

class Square extends Rectangle {
    constructor(length) {
        // 与 Rectangle.call(this, length, length) 相同
        super(length, length);
    }
}

// 派生类 Square 直接继承了基类的静态成员 create
var rect = Square.create(3, 4);

console.log(rect instanceof Rectangle); // true
console.log(rect.getArea()); // 12
console.log(rect instanceof Square); // false

3. 从表达式中派生类

在 ES6 中派生类的最强大能力: 或许就是能够从表达式中派生类. 只要一个表达式能够返回一个具有 [[Construct]] 属性以及原型的函数, 就可以对其使用 extends

// 从 ES5 风格的构造器中继承
function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

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

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

extends 后面能接受任意类型的表达式, 例如动态地决定所要继承的类:

function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

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

function getBase() {
    return Rectangle;
}

class Square extends getBase() {
    constructor(length) {
        super(length, length);
    }
}

// 也可以有效地创建混入
let SerializableMixin = {
    serialize() {
        return JSON.stringify(this);
    }
}

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

function mixin(...mixins) {
    var base = fucntion() {};
    Object.assigin(base.prototyp, ...mixins);
    return base;
}

class Square extends mixin(AreaMixin, SerializableMixin) {
    constructor(length) {
        super();
        this.length = length;
        this.width = length;
    }
}

var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"

任意表达式都能在 extends 关键字后使用, 但并非所有表达式的结果都是一个有效的类. 特别的, 下列表达式类型会导致错误:

  • null;
  • 生成器函数;

试图使用结果为上述值得表达式来创建一个新的类实例, 都会抛出错误, 因为不存在 [[Construct]] 可供调用

4. 继承内置对象

在 ES5 中, 通过继承机制来创建自定义的特殊数组类型, 这个不可能做到的

在 ES6 中的类, **其设计目的之一就是允许从内置对象进行继承. 为了达成这个目的, 类的继承模型与 ES5 或更早版本的传统继承模型有轻微差异: **

在 ES5 的传统继承中, this 的值会先被派生类( 例如 MyArray ) 创建, 随后基类构造器(例如 Array.apply() 方法)才被调用. 这意味着 this 一开始就是 MyArray 的实例, 之后才使用了 Array 的附加属性对其进行了装饰

在 ES6 基于类的继承中, this 的值会先被基类(Array)创建, 随后才被派生类的构造器(MyArray)所修改. 结果是 this 初始就拥有作为基类的内置对象的所有功能, 并能正确接受与之关联的所有功能

// 在 ES5 中尝试继承数组
function MyArray() {
    Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        writable: true,
        configurable: true,
        enumerable: true
    }
});

// MyArray 实例上的 lengt 属性以及数值属性, 其行为与内置数组并不一致, 因为这些功能并未被涵盖在 Array.apply() 或 数组原型中
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0

colors.length = 0;
console.log(colors[0]); // "red"

// ES6的继承内置对象
class MyArray extends Array {
    // 空代码块
}
// 行为与正规数组一直
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined

5. Symbol.species 属性

继承内置对象一个有趣的方面是: 任意能返回内置对象实例的方法, 在派生类上却会自动返回派生类的实例

class MyArray extends Array {
    // 空代码块
}

let items = new MyArray(1, 2, 3, 4);
    subitems = items.slice(1, 3);

console.log(items instanceof MyArray); // true
// 本意是返回一个 Array, 但是却返回了 MyArray
console.log(subitems instanceof MyArray); // true

**Symbol.species 属性在后台造成了这种变化 **

Symbol.species 知名符号被用于定义一个能返回函数的静态访问器属性, 每当类实例的方法(构造器除外) 必须创建一个实例时, 前面返回的函数就被用为新实例的构造器

下列内置类型都定义了 Symbol.species:

  • Array (所以自定义类型 MyArray 也继承了 Array的 Symbol.species)
  • ArrayBuffer
  • Map
  • Promise
  • RegExp
  • Set
  • 类型化数组

以上每个类型都拥有默认的 Symbol.species 属性, 其返回值为 `this, 意味着该属性总是会返回自身的构造器函数.

// 几个内置类型使用 species 的方式类似于此
class MyClass {
    // 此处只有 getter 而没有 setter, 这是因为修改类的 species 是不允许的
    static get [Symbol.species]() {
        return this;
    }
    constructor(value) {
        this.value = value;
    }
    
    clone() {
        return new this.constructor[Symbol.species](this.value);
    }
}

// 在 Array 派生出的类中, 可以重写 Symbol.species 来决定继承的方法应返回何种 类型的对象
class MyArray extends Array {
    static get [Symbol.species]() {
        return Array;
    }
}

let items = new MyArray(1, 2, 3, 4),
    subitems = items.slice(1, 3);

console.log(items instanceof MyArray); // true
// 这样, 使用 Array 继承方法就会返回 Array 实例
console.log(subitems instanceof Array); // true
console.log(subitems instanceof MyArray); // false

在类构造器中使用 new.target

在第三章中 new.target : 保存着使用 new 调用时的 this引用

同样可以在类构造器中使用 new.targer, 来判断类是被如何调用的

注意: 类构造器被调用时不能缺少 new, 因此 new.target 属性就始终会在类构造器内被定义, 于是 new.target 属性在类构造器内部就绝不会是 undefined

// 不发生继承时, new.target 就等于当前类
class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle); // true
        this.length = length;
        this.width = width;
    }
}
// new.target 就是 Rectangle
let obj = new Rectangle(3,4); // 输出 true

// 发生继承关系时, new.target 等于派生类
class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width;
    }
}

class Square extends Rectangle {
    constructor(length) {
        super(length, length)
    }
}

// new.target 就是 Square
var obj = new Square(3); // 输出 false

根据这个特性, 构造器能根据如何被调用而有不同行为, 并且这给了更改这种行为的能力

// 创建一个抽象基类(一种不能被实例化的类)
// 静态的基类
class Shape {
    constructor() {
        if (new.target === Shape) {
                throw new Error("This class cannot be instantiated directly.")
        }
    }
}

class Rectangle extends Shape {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

var x = new Shape(); // 抛出错误
var y = new Rectangle(3, 4); // 没有错误
console.log(y instanceof Shape); // true

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

推荐阅读更多精彩内容

  • 2007年9月1日,天高云淡,这是开学的日子。 “同学们,欢迎你们来到明徳高中。进入高中,我们的学习与生活将展...
    墨沫观阅读 183评论 0 0
  • 爱,是一种信仰。爱能够给人温暖,带给人幸福和快乐。人的思想无时不刻不在变化,但永恒不变的是爱以及爱的结果。 ...
    竹韵莲心阅读 476评论 0 0
  • 由于工作原因;有时候下午需要一个小时的午睡;一到时间点我会关好门窗;戴上眼罩;以为与外界大部分的声音与光线的...
    此生向阳而生阅读 238评论 0 0
  • 南乡子 文|兪秋 怅望西风。闲趁霜晴破影重。鬓白相思秋水逐。千簇,满目怜花眠中曲。
    兪秋阅读 619评论 1 6
  • 1,当你需要合并某分支的部分文件
    forjie阅读 2,015评论 0 0