写在开头
ES6
常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。
相关文章
- ES6常用但被忽略的方法(第一弹解构赋值和数值)
- ES6常用但被忽略的方法(第二弹函数、数组和对象)
- ES6常用但被忽略的方法(第三弹Symbol、Set 和 Map )
- ES6常用但被忽略的方法(第四弹Proxy和Reflect)
- ES6常用但被忽略的方法(第五弹Promise和Iterator)
- ES6常用但被忽略的方法(第六弹Generator )
- ES6常用但被忽略的方法(第七弹async)
- ES6常用但被忽略的方法(第九弹Module)
- ES6常用但被忽略的方法(第十弹项目开发规范)
- ES6常用但被忽略的方法(第十一弹Decorator)
- ES6常用但被忽略的方法(终弹-最新提案)
Class 函数
简介
- class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到。
// 传统写法
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
// class 写法
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
- 定义类的方法的时候,前面不需要加上
function
这个关键字,直接把函数定义放进去了就可以了。方法之间不需要逗号分隔,加了会报错。 类的所有方法都定义在类的prototype
属性上面。
class Point {
constructor() {...}
toString() {...}
toValue() {...}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
typeof Point // "function"
Point === Point.prototype.constructor // true
- 类的内部所有定义的方法,都是不可枚举的(non-enumerable)。这与 ES5 的行为不一致。
// es6
class Point {
constructor(x, y) {...}
toString() {...}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
// es5
var Point = function (x, y) {...};
Point.prototype.toString = function() {...};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
constructor 方法
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(Object);
}
}
new Foo() instanceof Foo // false
new Foo() instanceof Object // true
取值函数(getter)和存值函数(setter)
- 与
ES5
一样,在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。存值函数和取值函数是设置在属性的Descriptor
对象上的。
class MyClass {
constructor() {...}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123; // setter: 123
inst.prop; // 'getter'
const descriptor = Object.getOwnPropertyDescriptor(
MyClass.prototype, "prop"
);
"get" in descriptor // true
"set" in descriptor // true
表达式
- 属性表达式
let methodName = 'getArea'; class Square { constructor(length) {...} [methodName]() {...} }
- Class 表达式
const MyClass = class Me { getClassName() { return Me.name; } };
- Class 表达式,可以写出立即执行的 Class。
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('detanx'); person.sayName(); // "detanx"
注意点
-
严格模式
- 类和模块的内部,默认就是严格模式,所以不需要使用
use strict
指定运行模式。
- 类和模块的内部,默认就是严格模式,所以不需要使用
-
不存在提升
- 使用在前,定义在后,这样会报错。
new Foo(); // ReferenceError class Foo {}
-
name
属性- 由于本质上,
ES6
的类只是ES5
的构造函数的一层包装,所以函数的许多特性都被Class
继承,包括name
属性。
class Point {} Point.name // "Point"
name
属性总是返回紧跟在class
关键字后面的类名。
- 由于本质上,
-
Generator
方法- 如果某个方法之前加上星号(
*
),就表示该方法是一个Generator
函数。
- 如果某个方法之前加上星号(
-
this
的指向- 类的方法内部如果含有
this
,它默认指向类的实例。一旦单独使用该方法,很可能报错。
class Logger { printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); } } const logger = new Logger(); const { printName } = logger; printName(); // TypeError: Cannot read property 'print' of undefined
- 类的方法内部如果含有
静态方法
- 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
- 如果静态方法包含
this
关键字,这个this
指的是类,而不是实例。 - 父类的静态方法,可以被子类继承。
class Foo {
static classMethod() {
return 'detanx';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'detanx'
- 静态方法也是可以从
super
对象上调用的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', detanx';
}
}
Bar.classMethod() // "hello, detanx"
实例属性的新写法
- 实例属性除了定义在
constructor()
方法里面的this上面,也可以定义在类的最顶层。
class IncreasingCounter {
_count = 0
=> // 或写在constructor
// constructor() {
// this._count = 0;
// }
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
静态属性
- 静态属性指的是
Class
本身的属性,即Class.propName
,而不是定义在实例对象(this
)上的属性。
class Foo { }
Foo.prop = 1;
Foo.prop // 1
- 现在有一个 提案(目前处于
stage 3
) 提供了类的静态属性,写法是在实例属性的前面,加上static
关键字。
// 新写法
class Foo {
static prop = 1;
}
私有方法和私有属性
- 现有的解决方案
- 私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但
ES6
不提供,只能通过变通方法模拟实现。
- 做法是在命名上加以区别。
- 开发时约定以什么开头的属性或方法为私有属性。例如以
_
开头或者$
开头的为私有。
- 开发时约定以什么开头的属性或方法为私有属性。例如以
- 将私有方法移出模块
class Widget { foo (baz) { bar.call(this, baz); } } function bar(baz) { return this.snaf = baz; }
- 利用
Symbol
值的唯一性,将私有方法的名字命名为一个Symbol
值。ES6常用但被忽略的方法(第三弹Symbol、Set 和 Map )之Symbol的应用const bar = Symbol('bar'); const snaf = Symbol('snaf'); export default class myClass{ // 公有方法 foo(baz) { this[bar](baz); } // 私有方法 [bar](baz) { return this[snaf] = baz; } };
- 也不是绝对不行,
Reflect.ownKeys()
依然可以拿到它们。
const inst = new myClass(); Reflect.ownKeys(myClass.prototype) // [ 'constructor', 'foo', Symbol(bar) ]
- 也不是绝对不行,
- 私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但
- 私有属性的提案
- 目前,有一个 提案(
Stage 3
),为class
加了私有属性和方法。方法是在属性名和方法之前,使用#
表示。私有属性也可以设置getter
和setter
方法。
class Counter {
#xValue = 0;
constructor() {
super();
// ...
}
get #x() { return #xValue; }
set #x(value) {
this.#xValue = value;
}
}
- 私有属性和私有方法前面,也可以加上
static
关键字,表示这是一个静态的私有属性或私有方法。
new.target
属性
new
是从构造函数生成实例对象的命令。ES6
为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。如果构造函数不是通过new
命令或Reflect.construct()
调用的 ,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。Class
内部调用new.target
,返回当前Class
。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
var obj = new Rectangle(3, 4); // 输出 true
- 子类继承父类时,
new.target
会返回子类。 利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
继承
注意点
- 子类继承父类需要先调用
super
方法。class Point { /* ... */ } class ColorPoint extends Point { constructor() { } } let cp = new ColorPoint(); // ReferenceError
ColorPoint
继承了父类Point
,但是它的构造函数没有调用super
方法,导致新建实例时报错。
- 在子类的构造函数中,只有调用
super
之后,才可以使用this
关键字,否则会报错。class Point { constructor(x) { this.x = x; } } class ColorPoint extends Point { constructor(x, color) { this.color = color; // ReferenceError super(x); this.color = color; // 正确 } }
Object.getPrototypeOf()
Object.getPrototypeOf
方法可以用来从子类上获取父类。可以使用这个方法判断,一个类是否继承了另一个类。
Object.getPrototypeOf(ColorPoint) === Point // true
super 关键字
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
super
作为函数调用时,代表父类的构造函数。ES6
要求,子类的构造函数必须执行一次super
函数。否则JavaScript
引擎会报错。class A { constructor() { console.log(new.target.name); } } class B extends A { constructor() { super(); } } new A() // A new B() // B
super
虽然代表了父类A
的构造函数,但是返回的是子类B
的实例,super()
相当于A.prototype.constructor.call(this)
,super()
内部的this
指向的是B
。- 作为函数时,
super()
只能用在子类的构造函数之中,用在其他地方就会报错。
class A {} class B extends A { m() { super(); // 报错 } }
super
作为对象时- 普通方法中,指向父类的原型对象;静态方法(带
static
前缀的方法)中,指向父类。
class A { p() { return 2; } } class B extends A { constructor() { super(); console.log(super.p()); // 2 } } let b = new B();
super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的。如果属性定义在父类的原型对象上,super
就可以取到。
class A { constructor() { this.p = 2; } } class B extends A { get m() { return super.p; } } let b = new B(); b.m // undefined // 定义到原型上 class A {} A.prototype.x = 2; class B extends A { constructor() { super(); console.log(super.x) // 2 } } let b = new B();
- 在子类的静态方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例。
class A { constructor() { this.x = 1; } static print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } static m() { super.print(); } } B.x = 3; B.m() // 3
- 使用
super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
class A {} class B extends A { constructor() { super(); console.log(super); // 报错 } }
- 由于对象总是继承其他对象的,所以可以在任意一个对象中,使用
super
关键字。
var obj = { toString() { return "MyObject: " + super.toString(); } }; obj.toString(); // MyObject: [object Object]
- 普通方法中,指向父类的原型对象;静态方法(带
类的 prototype
属性和__proto__
属性
- 大多数浏览器的
ES5
实现之中,每一个对象都有__proto__
属性,指向对应的构造函数的prototype
属性。存在两条继承链。- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类。 - 子类
prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class A {} class B extends A {} B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true
- 子类的
- 类的继承模式实现
class A {} class B {} // B 的实例继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype); // B 继承 A 的静态属性 Object.setPrototypeOf(B, A); const b = new B();
Object.setPrototypeOf
方法的实现。
Object.setPrototypeOf = function (obj, proto) { obj.__proto__ = proto; return obj; }
- 类实例的
__proto__
属性的__proto__
属性,指向父类实例的__proto__
属性。子类的原型的原型,是父类的原型。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
原生构造函数的继承
- 原生构造函数是指语言内置的构造函数,通常用来生成数据结构。
ECMAScript
的原生构造函数大致有:Boolean()
、Number()
、String()
、Array()
、Date()
、Function()
、RegExp()
、Error()
、Object()
... ES6
可以自定义原生数据结构(比如Array
、String
等)的子类,这是ES5
无法做到的。例如实现一个自己的带其他功能的数组类。
class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]
x.revert();
x // [1, 2]
- 继承
Object
的子类,有一个行为差异。
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
- 上面代码中,
NewObj
继承了Object
,但是无法通过super
方法向父类Object
传参。这是因为ES6
改变了Object
构造函数的行为,一旦发现Object
方法不是通过new Object()
这种形式调用,ES6
规定Object
构造函数会忽略参数。
Mixin
模式的实现
Mixin
指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。使用的时候,只要继承这个类即可。
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 拷贝实例属性
}
}
}
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'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
// 使用
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}