一、简介
1、类的由来
JavaScript 语言中,生成实例对象
的传统方法是通过构造函数
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改写
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
constructor() 构造方法
this
关键字代表实例对象
- 方法前面不加
function
- 方法与方法之间不用
逗号
分隔 - 创建实例时用
new
类class是构造函数的另一种写法:
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
类的数据类型就是函数,类本身就指向构造函数
在类的实例上面调用方法,其实就是调用原型上的方法
class B {}
const b = new B();
b.constructor === B.prototype.constructor // true
prototype
对象的constructor()
属性,直接指向“类”的本身
Point.prototype.constructor === Point // true
类的内部所有定义的方法,都是不可枚举
的(non-enumerable)
2、constructor方法
constructor()
方法是类的默认方法
,通过new
命令生成对象实例时,自动调用
该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。
constructor()方法默认返回实例对象(即this)
,完全可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
constructor()函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
类必须使用new调用
,否则会报错。
3、类的实例
实例的属性除非显式定义在其本身
(即定义在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
proto 并不是语言本身的特性,不建议在生产中使用该属性,避免对环境产生依赖。
可以使用 Object.getPrototypeOf
方法来获取实例对象的原型,然后再来为原型添加方法/属性。
使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。
4、取值函数(getter)和存值函数(setter)
get和set,拦截该属性的存取行为
存值函数和取值函数是设置在属性的 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
存值函数和取值函数是定义在html属性的描述对象上面
5、属性表达式
类的属性名,可以采用表达式。
let methodName = 'getArea';
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
6、Class 表达式
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
Me
只在 Class 的内部
可用,指代当前类。(类的内部没用到的话,可以省略Me)
在 Class 外部
,用MyClass
引用
7、注意点
(1)严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
(2)不存在提升
类不存在变量提升(hoist)
(3)name 属性
函数
的许多特性都被Class
继承,包括name
属性。
class Point {}
Point.name // "Point"
(4)Generator 方法
方法前加上星号(*),表示是一个 Generator 函数
。
(5)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
由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错
解决方法:
- 在构造方法中绑定this
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
- 使用箭头函数
class Obj {
constructor() {
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myObj // true
箭头函数内部的this总是指向定义时所在的对象。
箭头函数处于构造函数内部,this总是指向实例对象
3. 使用Proxy(获取方法的时候,自动绑定this)
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());
二、静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
注意,静态方法的this
指的是类,而不是实例。
- 类中的静态方法可以调用静态方法
父类
的静态方法,可以被子类继承
- 静态方法也是可以从
super
对象上调用的
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
三、实例属性的新写法
实例属性除了定义在constructor()
方法里面的this
上面,也可以定义在类的最顶层
。
class IncreasingCounter {
_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;
// 新写法
class Foo {
static prop = 1;
}
五、私有方法和私有属性
1、概念
私有方法
和私有属性
,是只能在类的内部访问
的方法和属性,外部不能访问。
2、私有属性的提案
用#
表示私有属性和私有方法
class Foo {
#a;
#b;
constructor(a, b) {
this.#a = a;
this.#b = b;
}
#sum() {
return this.#a + this.#b;
}
printSum() {
console.log(this.#sum());
}
}
- 私有属性也可以设置
getter 和 setter
方法 - 私有属性不限于从this引用,只要是在类的内部,
实例也可以引用私有属性
。
-私有属性和私有方法
前面,也可以加上static
关键字,表示这是一个静态的私有属性或私有方法
。
3、in 运算符
in
运算符可以用来判断私有属性
。
class A {
use(obj) {
if (#foo in obj) {
// 私有属性 #foo 存在
} else {
// 私有属性 #foo 不存在
}
}
}
in
可以和this
一起使用- 判断
私有属性
时,in只能用在定义该私有属性的类的内部
- 子类从父类继承的私有属性,也可以使用in运算符来判断。
in
运算符对于Object.create()
、Object.setPrototypeOf
形成的继承,是无效的,因为这种继承不会传递私有属性
六、静态块
静态块
(static block),允许在类的内部设置一个代码块,在类生成时运行一次,主要作用是对静态属性进行初始化
。还有一个作用,就是将私有属性与类的外部代码分享
。
- 每个类只能有
一个静态块
,在静态属性声明后运行。静态块的内部不能有return语句
。 - 静态块内部可以使用
类
名或this
,指代当前类
。
class C {
static x = 1;
static {
this.x; // 1
// 或者
C.x; // 1
}
}
七、new.target 属性
new
是从构造函数生成实例对象
的命令。new.target
属性一般用在构造函数
之中,返回new
命令作用于的那个构造函数
。- 如果构造函数不是通过
new
命令或Reflect.construct()
调用的,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 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会报错。
八、class的继承
1、简介
Class 可以通过extends
关键字实现继承,让子类继承父类的属性和方法。
super
在这里表示父类的构造函数
,用来新建一个父类的实例对象
。
子类
必须在constructor()
方法中调用super()
,否则就会报错。
继承机制:
- ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”
- ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。
- 在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。
- 除了私有属性和私有方法,父类的所有属性和方法,都会被子类继承,其中包括静态方法。
2、Object.getPrototypeOf()
Object.getPrototypeOf()
方法可以用来从子类上获取父类。
class Point { /*...*/ }
class ColorPoint extends Point { /*...*/ }
Object.getPrototypeOf(ColorPoint) === Point
// true
可以使用这个方法判断,一个类是否继承了另一个类
3、super关键字
super
这个关键字,既可以当作函数
使用,也可以当作对象
使用。
(1)第一种情况,super作为函数调用时,代表父类的构造函数
注意,super
虽然代表了父类A的构造函数
,但是返回的是子类B的实例
,即super内部的this
指的是B的实例
,因此super()
在这里相当于A.prototype.constructor.call(this)
。
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
new.target
指向当前正在执行的函数
super()只能用在子类的构造函数之中,用在其他地方就会报错。
(2)第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
- 由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
- 在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。
- this指向子类实例,所以如果通过super对某个属性赋值,这时
super
就是this
,赋值的属性会变成子类实例的属性
。 子类的静态方法
中通过super
调用父类的方法时,方法内部的this
指向当前的子类
,而不是子类的实例- 使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错
- super.valueOf()表明super是一个对象
- 由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字
4、类的 prototype 属性和__proto__属性
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
Object.setPrototypeOf
方法的实现:
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
第一种,子类继承Object类。
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
第二种情况,不存在任何继承。
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
5、实例的 proto 属性
子类实例的__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
6、原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。
ECMAScript 的原生构造函数:
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
extends
关键字不仅可以用来继承类
,还可以用来继承原生的构造函数
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]
7、Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。
实现: 将多个类的接口“混入”(mix in)另一个类
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);
}
}
}
原文链接: