一、class简介
1.1 简单例子
-
通过
class
关键字,可以定义类。 -
es5
语法
function Point (x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return `this.x = ` + this.x + ', this.y = '+ this.y;
};
var p = new Point(10, 20);
es6
语法:class
改写上面的代码
// 定义类
class Point {
constructor (x, y) {
this.x = x;
this.y = y;
}
toString () { // 等价于 Point.prototype.toString = function(){}
return `(${this.x},${this.y})`;
}
}
// new 命令
var p = new Point(10, 20);
console.log(p.toString()); // (10,20)
typeof Point; // function
1.2 class注意事项
-
定义“类”的方法的时候,前面不需要加上
function
这个关键字,直接把函数定义放进去了就可以了。 -
方法之间不需要逗号分隔,加了会报错。
-
类必须使用
new
调用,否则会报错;普通构造函不用new
也可以执行。。 -
类的所有方法都定义在类的
prototype
属性上面。 - 类不存在变量提升(
hoist
)。- 保证子类在父类之后定义。
prototype
对象的constructor
属性,直接指向“类”的本身。
Point.prototype.constructor === Point // true
- 类的内部所有定义的方法,都是不可枚举的(
non-enumerable
)。
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype);
// []
// 如果是es5,是可枚举的: ["toString"]
Object.getOwnPropertyNames(Point.prototype);
// ["constructor","toString"]
- 类的属性名,可以采用表达式。
let methodName = 'getData';
class Square {
constructor () {
//...
}
[methodName] () {
// ...
}
}
1.3 一次向类添加多个方法 Object.assign
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
1.4 constructor 方法
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。- 一个类必须有
constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。 constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象。
class Foo {
constructor () {
// console.log(this); // Foo {}
return Object.create(null);
}
}
new Foo() instanceof Foo; //false
// new Foo() === > this ====> Object {}
1.5 类的实例对象
- 与
ES5
一样,实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上(即定义在class
上)。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
- 类的所有实例共享一个原型对象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
- 生产环境中,我们可以使用
Object.getPrototypeOf
方法来获取实例对象的原型,然后再来为原型添加方法/属性。
1.6 class表达式
const MyClass = class Me {
getName () {
// console.log(Me); // class Me {getName () {console.log(Me, this);return Me.name;}}
return Me.name;
}
}
let inst = new MyClass();
inst.getName(); // Me
console.log(Me.name); // ReferenceError: Me is not defined
const MyClass = class {};
- 这个类的名字是
MyClass
而不是Me
,Me
只在Class
的内部代码可用,指代当前类。 Me
只在Class
内部有定义。-
如果类的内部没用到的话,可以省略
Me
。 - 采用
Class
表达式,可以写出立即执行的Class
。
let person = class {
constructor (name) {
this.name = name;
}
sayName () {
return this.name;
}
}('jm');
person.sayName(); // jm
1.7 私有方法
ES6
不提供私有方法,只能通过变通方法模拟实现。- 1.在命名上加以区别
- 这种命名是不保险的,在类的外部,还是可以调用到这个方法。
class Widget { // 公有方法 foo (baz) { this._bar(baz); } // 私有方法 _bar(baz) { return this.snaf = baz; } // ... }
- 2.将私有方法移出模块,因为模块内部的所有方法都是对外可见的。
class Widget { // foo是公有方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。 foo (baz) { bar.call(this, baz); } // ... } function bar(baz) { return this.snaf = baz; }
- 3.利用
Symbol
值的唯一性,将私有方法的名字命名为一个Symbol
值。
// bar和snaf都是Symbol值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果。 const bar = Symbol('bar'); const snaf = Symbol('snaf'); export default class myClass{ // 公有方法 foo(baz) { this[bar](baz); } // 私有方法 [bar](baz) { return this[snaf] = baz; } // ... };
- 1.在命名上加以区别
### 1.8 私有属性
- ES6 不支持私有属性
- 为
class
加私有属性:方法是在属性名之前,使用#
表示。
class Point {
// #x就表示私有属性x,在Point类之外是读取不到这个属性的。
#x;
// 初始化:#x = 0;
constructor (x = 0) {
#x += x;
// 初始化后: #x; // 此时 #x = 0;
}
get x () {
return #x;
}
set x (value) {
#x += value;
}
}
- 私有属性与实例的属性是可以同名的(比如,#x与get x())。
- 私有属性可以指定初始值,在构造函数执行时进行初始化。
- 也可以用来写私有方法
class Foo {
#a;
#b;
#sum() { return #a + #b; }
printSum() { console.log(#sum()); }
constructor(a, b) { #a = a; #b = b; }
}
1.9 私有方法
- 类的方法内部如果含有
this
,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class Logger {
printName (name = 'there') {
this.print(`Hello,${name}`);
}
print (text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger; // 等价于 const printName = logger.printName; 其实就是解构
printName(); // TypeError: Cannot read property 'print' of undefined
const { printName } = logger;
这一行代码,导致this
指向的不是Logger
类的实例,而是指向该方法运行时所在的环境,所以因找不到print
方法而导致报错。
解决方法
- 1.在构造方法中绑定
this
class Logger {
constructor () {
this.printName = this.printName.bind(this);
}
// ...
}
- 2.使用箭头函数
class Logger {
construtor () {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
}
}
// ...
}
- 3.使用
Proxy
,获取方法的时候,自动绑定this
。 ```js function selfish (target) { const cache = new WeakMap(); const handler = { get (target, key) { const value = Reflect.get(target, key); if (typeof value !== ‘function’) { return value; } if (!cache.has(value)) { cache.set(value, value.bind(target)); } return cache.get(value); } }; const proxy = new Proxy(target, handler); return proxy; }
const logger = selfish(new Logger());
### 1.10 name属性
* `name`属性总是返回紧跟在`class`关键字后面的类名。
```js
class Point {}
Point.name // "Point"
1.11 Class 的取值函数(getter)和存值函数(setter)
- 与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
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'
// prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。
- 存值函数和取值函数是设置在属性的
Descriptor
对象上的。
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"
);
"get" in descriptor // true
"set" in descriptor // true
- 报错:
Uncaught RangeError: Maximum call stack size exceeded
,demo- 原因:在构造函数中执行
this.name = name
的时候,就会去调用set name
,在set name
方法中,我们又执行this.name = name
,进行无限递归,最后导致栈溢出(RangeError
)。
class Person {
constructor(name) {
this.name = name
}
// getter
get name() {
return this.name
}
// setter
set name(name) {
this.name = name
}
}
let p1 = new Person();
p1.name = 'jm'
// 报错: Uncaught RangeError: Maximum call stack size exceeded
- 解决方法:demo
- 补充:以上
name
的getter
和setter
只是给name
自定义存取值行为,开发者还是可以通过_name
绕过getter
和setter
获取name
的值。
class Person {
constructor(name) {
this.name = name
}
// getter
get name() {
return this._name
}
// setter
set name(name) {
this._name = name
}
}
let p1 = new Person();
p1.name = 'jm'
console.log(p1.name) // 'jm
1.12 Class 的 Generator 方法
- 如果某个方法之前加上星号(
*
),就表示该方法是一个Generator
函数。
class Foo {
constructor(...args) {
this.args = args;
}
// Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。
// Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world
1.13 Class 的静态方法
- “静态方法”:在一个方法前,加上
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用。- 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。
- 如果静态方法包含
this
关键字,这个this
指的是类,而不是实例。
class Foo {
static bar () {
// this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz
this.baz();
}
// 静态方法可以与非静态方法重名。
static baz () {
console.log('hello');
}
baz () {
console.log('world');
}
}
// bar 直接在Foo类上调用
Foo.bar() // hello
// bar 不能在Foo类的实例上调用,如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法
var foo = new Foo();
foo.bar(); // TypeError: foo.classMethod is not a function
- 父类的静态方法,可以被子类继承。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
- 静态方法也是可以从
super
对象上调用的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
1.14 Class 的静态属性和实例属性
-
静态属性指的是
Class
本身的属性,即Class.propName
,而不是定义在实例对象(this
)上的属性。 -
例子: 为
Foo
类定义了一个静态属性prop
。- 目前,只有这种写法可行,因为
ES6
明确规定,Class
内部只有静态方法,没有静态属性。
- 目前,只有这种写法可行,因为
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
// 以下两种写法都无效
class Foo {
// 写法一
prop: 2
// 写法二
static prop: 2
}
Foo.prop // undefined
有一个静态属性的提案,对实例属性和静态属性都规定了新的写法。
- 1.类的实例属性
- 类的实例属性可以用等式,写入类的定义之中。
class MyClass {
myProp = 42; // myProp就是MyClass的实例属性。
constructor() {
console.log(this.myProp); // 42
}
}
- 2.类的静态属性
- 类的静态属性只要在上面的实例属性写法前面,加上
static
关键字就可以了。
- 类的静态属性只要在上面的实例属性写法前面,加上
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
// 老写法
class Foo {
// ...
}
Foo.prop = 1;
// 新写法
class Foo {
static prop = 1;
}
1.15 new.target属性
new
是从构造函数生成实例的命令。ES6
为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。- 如果构造函数不是通过
new
命令调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
- 如果构造函数不是通过
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, '张三'); // 报错
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 Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var obj = new Square(3); // 输出 false
- 利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
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); // 正
- 上面代码中,
Shape
类不能被实例化,只能用于继承。
注意,在函数外部,使用
new.target
会报错。