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.propotype.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
类的原型上。
⚠ 注意
__porto__
并不是语言本身的特性,而是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎都提供这个私有属性,但依旧不建议在生产中使用,避免对环境产生依赖。生产环境中,可以使用 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
,获取方法的时候,自动绑定this
jsfunction 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
// world
Class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 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 面向对象编程的重要组成部分。