[JS] ECMAScript 6 - Class : compare with c#
Ref: Class 的基本语法
Ref: Class 的基本继承
许多面向对象的语言都有修饰器(Decorator)函数,用来修改类的行为。目前,有一个提案将这项功能,引入了 ECMAScript。
Ref: JavaScript 中的 this 用法以及 call(apply) 的理解
Ref: JavaScript Object-Oriented Programming Tutorial - OOP with E6【简单介绍引入了类后,带来的简单写法】
基础概念
Class关键字
(1) 传统写法
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);
(2) ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
类的数据类型就是函数,类本身就指向构造函数。
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } }
类的方法
class Point { constructor() { // 都定义在类的prototype
属性上面 // ... } toString() { // 都定义在类的prototype
属性上面 // ... } toValue() { // 都定义在类的prototype
属性上面 // ... } }
类的所有方法都定义在类的prototype
属性上面。也就是等同于:
Point.prototype = { constructor() {}, toString() {}, toValue() {}, };
也就是等价:
在类的实例上面调用方法,其实就是调用原型上的方法。
class B {} let b = new B(); b.constructor === B.prototype.constructor // true ----> 其实就是调用原型上的方法。
扩展技巧:
Object.assign
方法可以很方便地一次向类添加多个方法。
class Point { constructor(){ // ... } } Object.assign(Point.prototype, { toString(){}, toValue(){} });
内部属性方法不可枚举:
可枚举:
var Point = function (x, y) { // ... }; Point.prototype.toString = function() { // ... }; Object.keys(Point.prototype) // ["toString"]
Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"] ---------------------------------------------------------------------
不可枚举: class Point { constructor(x, y) { // ... } toString() { // ... } } Object.keys(Point.prototype) // [] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]
严格模式:
类和模块的内部,默认就是严格模式,所以不需要使用use strict
指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。
Goto: [JS] Why "strict mode" here
类的实例对象
定义了一个空的类Point
,JavaScript 引擎会自动为它添加一个空的constructor
方法。Constructor返回this,也就是指向自己。
class Point { // ... } // 报错 var point = Point(2, 3); // 正确 var point = new Point(2, 3); // 只能new,这样才更为接近“类”
对象方法调动的实际上都是prototype上的类型。
//定义类 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { // 要注意:其实是定义在原型prototype上 return '(' + this.x + ', ' + this.y + ')'; } } ---------------------------------------------------- var point = new Point(2, 3); point.toString() // (2, 3) point.hasOwnProperty('x') // true,因为上面给 this.x赋值 point.hasOwnProperty('y') // true,因为上面给 this.y赋值 point.hasOwnProperty('toString') // false point.__proto__.hasOwnProperty('toString') // true
Class 表达式
使用表达式的形式定义。
// 类的名字是MyClass
const MyClass = class Me { // Me只在class内部代码中可用;如果类的内部没用到的话,可以省略Me。
getClassName() {
return Me.name; // Me指代当前类
}
};
这么下,可用写成一个立即执行的类(函数):
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('张三'); person.sayName(); // "张三"
不存在变量提升
如果class
被提升到代码头部,而let
命令是不提升的,将导致Bar
继承Foo
的时候,Foo
还没有定义。
{ let Foo = class {}; class Bar extends Foo { // <---- 如果class提升,也就是提升到let Foo之前,返回会引来麻烦 } }
私有方法和私有属性
方法一:
私有方法是常见需求,但 ES6 不提供。【自己通过命名约定】
方法二:
将私有方法移出模块,因为模块内部的所有方法都是对外可见的。
class Widget { foo (baz) { //foo
是公有方法 bar.call(this, baz); // 这使得bar
实际上成为了当前模块的私有方法 } // ... }
----------------------------------------------------- function bar(baz) { return this.snaf = baz; }
方法三:
将私有方法的名字命名为一个Symbol
值。
const bar = Symbol('bar'); const snaf = Symbol('snaf'); export default class myClass{ // 公有方法 foo(baz) { this[bar](baz); } // 私有方法 [bar](baz) { return this[snaf] = baz; } // ... };
私有属性当前只是提案 - 暂略
this 的指向
这里主要是讲如何绑定this的问题,让this的指向比较可控。
(1) 这里找不到print,因为运行时this变为了全局环境。
class Logger {
printName(name = 'there') { this.print(`Hello ${name}`); // 这里的this
,默认指向Logger
类的实例 } print(text) { console.log(text); } }
----------------------------------------------------------------------- const logger = new Logger(); const { printName } = logger; printName(); // 将这个方法提取出来单独使用了
// TypeError: Cannot read property 'print' of undefined
(2) 改进:在构造方法中绑定this
,这样就不会找不到print
方法了。
(3) 使用箭头函数,使this只跟定义时的位置有关。
(4) 使用Proxy
,获取方法的时候,自动绑定this
。【暂时不懂】
name 属性
ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class
继承,包括name
属性。
class Point {} Point.name // "Point"
取值函数(getter)和存值函数(setter)
class MyClass {
constructor() { // ... }
----------------------------------------------- get prop() { return 'getter'; }
set prop(value) { // value是属性的value,通过__.prop = 123 等号的方式传过来的 console.log('setter: '+value); } }
----------------------------------------------- let inst = new MyClass(); inst.prop = 123; // setter: 123 inst.prop // 'getter'
Generator 方法
静态方法
(1) 加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用。而不是生成实例后调用。
class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // 生成实例后调用是不行的! // TypeError: foo.classMethod is not a function
(2) 如果静态方法包含this
关键字,这个this
指的是类,而不是实例。
class Foo { static bar () { this.baz(); }
static baz () { console.log('hello'); }
baz () { // 静态方法可以与非静态方法重名 console.log('world'); } } Foo.bar() // hello
(3) 父类的静态方法,可以被子类继承
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod() // 'hello'
(4) 静态方法也是可以从super
对象上调用的。
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod() // "hello, too"
Class 的静态属性和实例属性
(1)类的实例属性
class MyClass { myProp = 42; // 使用的是等号 constructor() { console.log(this.myProp); // 42 } }
以前,我们定义实例属性,只能写在类的constructor
方法里面。
class ReactCounter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } } ---------------------------------------------------------------- 新的写法,可以不在constructor
方法里面定义
class ReactCounter extends React.Component { state = { count: 0 }; }
(2)类的静态属性
ES6 明确规定,Class 内部:只有静态方法,没有静态属性。
那就暂时定义在外面:
class Foo {
}
Foo.prop = 1; // 定义在外面,为Foo
类定义了一个静态属性prop
Foo.prop // 1
--------------------------------------------------------
// 新写法
class Foo {
static prop = 1;
}
new.target 属性
ES6 为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。
这个属性可以用来确定构造函数是怎么调用的。
应用1. 构造函数只能通过new
命令调用
function Person(name) { if (new.target !== undefined) { this.name = name; } else { throw new Error('必须使用 new 命令生成实例'); } } // 另一种写法 function Person(name) { if (new.target === Person) { this.name = name; } else { throw new Error('必须使用 new 命令生成实例'); } } -------------------------------------------------------------- var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
应用2. 不能独立使用、必须继承后才能使用的类
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); // 正确
注意,在函数外部,使用new.target
会报错。