Class的基本语法
传统的 JavaScript 中,生成实例对象的传统方法是通过构造函数。但是这种写法与面向对象语言的写法差异大,ES6 引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。
简介
ES6 的 class 可以看作一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法让对象原型的写法更加清晰。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}⚠ 注意
定义类时,前面不需要加 function ,直接将函数定义放进去即可。构造函数的名称和类名需要相同,且不需要加 function 关键字。
类的数据类型就是函数,类本身就指向构造函数。使用的时候,也是直接对类使用 new 命令,跟构造函数的用法完全一致。
构造函数的 prototype 属性在 ES6 的 “类”上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。由于类的方法都是定义在 prototype 对象上面,所以类的新方法可以添加在 prototype 对象上面。Object.assign() 方法可以一次向类添加多个方法。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
}
// 等同于
Object.assign(Point.prototype, {
toString(){},
toValue(){}
})类的 prototype 对象上的 constructor 方法,直接指向“类”本身。
类的内部所有定义的方法,都是不可枚举的(non-enumerable)。ES5 的构造函数,所有的属性和方法都是可枚举的。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString","toValue"]
let Line = function(x, y) {
// ...
}
Line.prototype.toString = function() {}
Object.keys(Line.prototype) // ["toString"]
Object.getOwnPropertyNames(Line.prototype) // ["constructor","toString"]类的属性名可以采用表达式。
let name = 'myName';
class MMethod {
[name]() {
// ...
}
}严格模式
类和模块的内部,默认就是严格模式,所以不需要使用 use strict 指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。
constructor方法
constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。
class Point {
}
// 等同于
class Point {
constructor() {}
}constructor 方法默认返回实例对象(即 this ),也可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo // false类必须使用 new 来调用,否则会报错。这是它和普通构造函数的主要区别,后者不用 new 也能执行。
class Foo {
constructor() {
return Object.create(null);
}
}
Foo() // TypeError: Class constructor Foo cannot be invoked without 'new'类的实例对象
与 ES5 相同点:
- 生成实例对象写法也是用的
new命令,否则报错 - 和 ES5 一样,实例的属性除非显示定义在其本身(即定义在
this上),否则都是定义在原型上(即定义在class上) - 类所有实例共享一个原型对象
查看代码
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
let point = new Point(2, 3);
let point1 = new Point(3, 1);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
point.__proto__ === Point.prototype // true上述代码中,x 和 y 属性定义在实例对象 point 上。toString 方法定义在 Point 类的原型上。
⚠ 注意
__proto__ 并不是语言本身的特性,而是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JavaScript 引擎都提供这个私有属性,但依旧不建议在生产中使用,避免对环境产生依赖。生产环境中,可以使用 Object.getPrototypeOf() 方法来获取实例对象的原型,然后使用 Object.prototype.hasOwnProperty() 方法来判断属性是定义在实例对象上还是定义在原型上。
var p1 = new Point(1, 2)
var p2 = new Point(5, 2)
p1.__proto__.printName = function() { return 'Oops' }
p1.printName() // "Oops"
p2.printName() // "Oops"
let p3 = new Point(3, 4)
p3.printName() // "Oops"Class表达式
与函数一样, Class 也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name // Error: Me is not defined上述代码表明,Me 只在 Class 的内部可用,指代当前 Class。如果内部没有使用到,则可以省略。
const MyClass = class { /* ... */ };采用 Class 表达式,可以写出立即执行的 Class。下方代码 person 就是一个立即执行的 Class 实例。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"不存在变量提升
类没有变量提升(hoisting),这一点与 ES5 完全不同。这是因为 ES6 不会把类的声明提升到作用域的顶部,涉及到继承的知识点,必须保证子类在父类之后。
new Foo(); // ReferenceError
class Foo {}
{
let Bar = class {}
class Baz extends Bar {}
}Baz 继承 Bar 时,Bar 已经定义了。如果 Class 存在变量提升,class 会被提升到代码头部,而 let 没有提升,导致继承时 Bar 还没定义造成报错。
私有方法
私有方法 ES6 没有提供,需要变通方法来模拟实现。有三种方法可以实现:
命名上加以区别
jsclass A { // 公有方法 foo(bar) { // some code } // 私有方法 _bar() { // some code } }将私有方法移出模块,模块内部方法对外均可见
jsclass B { foo(bar) { bar.call(this, bar) } } function bar(baz) { return this.snaf = baz }利用
Symbol值唯一特性把私有方法命名为Symbol值jsconst bar = Symbol('bar') class C { [bar] () { // some code } }
私有属性
与私有方法类似,私有属性也不能从外部访问,只能通过类的内部方法访问。目前,私有属性只能通过 # 来定义。
class Point {
#x;
constructor(x = 0) {
#x = +x;
}
get x() { return #x }
set x(value) { #x = +value }
}私有属性可以指定初始值,在构造函数执行时进行初始化。
class Point {
#x = 0;
#y = 0;
constructor(x = #x, y = #y) {
#x = +x;
#y = +y;
}
}之所以用 # 表示而不用 private 关键字是因为 JavaScript 是一门动态语言,使用独立符号更可靠。该提案只规定私有属性的写法,但是也能用来编写私有方法。
class A {
#a;
#b;
#sum() { return #a + #b }
constructor(a, b) {
#a = a;
#b = b;
}
sum() { return #sum() }
}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上面代码中,printName 方法中的 this 指向的是 undefined,导致找不到 print 方法而报错。
解决方法有三个:
在构造方法中绑定
this,这样就不会找不到print方法了。jsclass Logger { constructor() { this.printName = this.printName.bind(this); } // ... }使用箭头函数。
jsclass Logger { constructor() { this.printName = (name = 'there') => { this.print(`Hello ${name}`); }; } }使用
Proxy,获取方法的时候,自动绑定thisjsfunction 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());
name属性
本质上,ES6 的类只是 ES5 的构造函数一层包装,所以函数许多特性都被 Class 继承,包括 name 属性。
class Point {}
Point.name // "Point"name 属性总是返回紧跟在 class 关键字后面的类名。
Class的取值函数(getter)和存值函数(setter)
与 ES5 一样,在 Class 内部可以使用 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'Class的Generator方法
如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// worldClass的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function父类的静态方法可以被子类继承,静态方法可以在 super 对象调用。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}Class的静态属性和实例属性
静态属性指的是 Class 本身的属性,即 Class.propname,而不是定义在实例对象 this 上的属性。
class Foo {
}
Foo.num = 1
Foo.num // 1目前只能用这种写法,ES6 明确规定 class 内部只有静态方法,没有静态属性。
class Foo {
static myStaticProp: 42 // 错误写法
prop: 42 // 错误写法
}
Foo.prop // undefined
Foo.myStaticProp // undefined目前有一个提案关于实例属性和静态属性都规定了新写法。实例属性直接写在类的定义之中;静态属性在实例属性前加一个 static 关键字即可。同时为了更强的可读性,允许已经在构造器 constructor 里定义过的实例属性直接列出。
class Foo {
// 实例属性
bar = 'hello';
// 静态属性
static baz = 42;
state
constructor() {
console.log(this.bar) // 'hello'
this.state = {
count: 0
}
}
}new target 属性
new 是从构造函数生成的实例命令,ES6 为 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。 - 子类继承父类时,
new.target会返回子类。 - 在函数外部,使用
new.target会报错。
总结
ES6 引入的 class 关键字为 JavaScript 提供了一种新的面向对象编程方式,使得类的定义和实例化更加直观和符合传统面向对象语言的风格。class 可以看作是构造函数的语法糖,它通过更简洁的语法封装了原型继承的细节。在类中,可以定义构造函数 constructor 来初始化实例属性,以及定义实例方法和静态方法。实例方法定义在类的 prototype 属性上,而静态方法则直接定义在类上,不会被实例继承。类的所有方法默认都是不可枚举的,这与 ES5 中构造函数的行为有所不同。
类的实例化使用 new 关键字,这与构造函数的调用方式一致。类的实例对象会继承类的原型属性和方法。类的属性名可以使用表达式,提供了更大的灵活性。此外,类内部默认是严格模式,无需显式声明。
ES6 的类不支持变量提升,类必须在声明后才能使用,这有助于避免一些常见的错误。类的私有方法和属性可以通过特定的命名约定或使用 Symbol 来模拟实现,但 ES6 本身并未提供原生的私有成员支持。私有属性可以通过 # 前缀来定义,这是后续提案中引入的特性。
类中的 this 指向实例对象,但在方法单独使用时可能会导致 this 失效,因此需要通过绑定或箭头函数等方式来确保 this 的正确指向。类还支持 getter 和 setter 方法,用于拦截属性的访问和赋值操作。此外,类可以定义 Generator 方法,以及使用 static 关键字定义静态方法和静态属性。
new.target 属性允许在构造函数中检测是否通过 new 调用,从而实现不同的逻辑分支。总的来说,ES6 的类提供了一种更加清晰和易于理解的方式来创建对象和实现继承,是 JavaScript 面向对象编程的重要组成部分。