对比 构造函数
ES5 构造函数
function Point(x, y) {
this.x = x
this.y = y
}
Point.prototype.toString = function() {
return `(${this.x}, ${this.y})`
}
ES6 引入了类这个概念,作为对象的模板。class 比构造函数更清晰,更像面向对象编程的语法。
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return `(${this.x}, ${this.y})`
}
}
constructor方法就是构造方法, 对应Es5 的构造函数Point。
类的方法之间不需要逗号,加了会报错。
class Point {}
typeof Point // "function"
Point === Point.protoype.constructor // true
上例: 类的数据类型就是函数,类本身就指向构造函数。
使用时, 直接对类使用new 命令,跟构造函数的用法基本一致。
class Bar {
doStuff() {
console.log('stuff')
}
}
var b = new Bar()
b.doStuff() // 'stuff'
构造函数的prototype属性,在 Es6的‘类’上继续存在。实际上,类的所有方法都定义在类的 prototype 属性上。
class Point {
constructor() {}
toString() {}
toValue() {}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {}
}
在类的实例上调用方法,其实就是调用原型上的方法。
class B {}
let b = new B()
b.contructor === B.prototype.constructor // true
类内部定义的所有方法,都是不可枚举的。
class Point {
constructor(x, y) {}
toString() {}
}
Object.keys(Point.prototype) // []
Object.getOwnPropertyNames(Point.prototype) // ['constuctor', 'toString']
上例:toString 方法是Point 类内部定义的方法,它是不可枚举的。这一点与Es5的行为不一致。
let Point = function (x, y) {}
Point.prototype.toString = function() {}
Object.keys(Point.prototype) // ['toString']
Object.getOwnPropertyNames(Point.prototype) // ['constructor', 'toString']
你在 Es5 构造函数 prototype 定义的方法是可枚举的。
constructor 方法
constructor 方法是类的默认方法,用new 命令生成对象时,会自动调用该方法。一个类必须有constructor 方法,如果没有显示定义,JavaScript 引擎会自动添加一个空的constructor 方法.
class Point {}
// 等同于
class Point {
constructor() {}
}
constructor 方法默认返回实例对象(即this), 但完全可以返回另一个对象。
class Foo {
constructor() {
return Object.create(null)
}
}
new Foo() instanceof Foo // false
上例: constructor 函数返回一个全新的对象,结果导致实例对象不是Foo 类的实例。
类必须使用new 调用,否则会报错。但是普通构造函数不用new 也可以执行。
类的实例
生成类的实例,必须使用new命令。
class Point {}
var point = Point(2, 3) // 报错
var point2 = new Point(2, 3) // 正确
实例的属性都定义在原型上(即定义在class上),除非显示定义在其本身(即定义在this 对象上)。
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
上例: x,y 实例对象自身的属性,定义在 this 上。toString 是原型上的属性, 定义在Point 类上。
与 Es5 一样, 类的所有实例共享一个原型对象。
var p1 = new Point(2, 3)
var p2 = new Point(3, 2)
p1.__proto__ === p2.__proto__ // true
proto 不是语言本身的特性,这是各大厂商在具体实现上添加的私有属性,虽然目前很多现代的浏览器的JS引擎都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。在生产环境中,使用Object.getprototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。
let p1 = new Point(2, 3)
let p2 = new Point(3, 2)
p1.__proto__.printName = function() { return 'Oops' }
p1.printName() // 'Oops'
p2.printName() // 'Oops'
let p3 = new Point(4, 3)
p3.printName() // 'Oops'
使用实例的proto 属性改写原型,会改变‘类’的原始定义,影响所有实例。
取值函数(getter) 和 存值函数(setter)
与Es5一样,在 ‘类’的内部可以使用 get 和 set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
get prop() {
return 'getter'
}
set prop(value) {
console.log(value)
}
}
let inst = new MyClass()
inst.prop = 123 // setter: 123
inst.prop // 'getter'
存值函数和取值函数是设置在属性的Descriptor 对象上的。
class CustomHTMLElement {
constructor(elemet) {
this.element = element
}
get html() {
return this.element.innerHTML
}
set html(value) {
this.element.innnerHTML = value
}
}
var decriptor = Object.getOwnPropertyDescriptor(
CustomHTMLElement.proptotype, 'html'
);
'get' in descriptor // true
'ste' in descriptor // true
属性表达式
类的属性名,可以采用表达式
let methodName = 'getArea'
class Square {
constructor(length) {}
[methodName]() {
console.log(methodName)
}
}
let squ = new Square()
squ.getArea() // 'getArea'
class 表达式
与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name
}
getOutClassName() {
return MyClass.name
}
}
let inst = new MyClass()
inst.getClassName() // me
inst.getOutClassName() // me
let myInst = new Me() // ReferenceError: Me is not defined
上例: Me 只能在Class 内部使用,指代当前类,在Class 外部只能使用MyClass 引用。
如果类的内部没有使用到的话,可以省略Me.
const MyClass = class {}
采用Class 表达式,可以写出立即执行的Class
let person = new class {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}('张三')
person.sayName() // ‘张三’
注意点
严格模块
类和模块的内部,默认都是严格模式
不存在提升
类不存在变量提升
new Foo() // ReferenceError
class Foo() {}
name 属性
name 属性总是返回紧跟在class 关键字后面的类名
class Point {}
point.name // 'Point'
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)
}
上例: Symbol.iterator 方法前有一个星号,表示该方法是一个Generator 函数。Symbol.iterator 方法返回一个Foo 类的默认遍历器,for…of 循环自动调用这个遍历器。
this 指向
类的方法内部的 this ,默认指向类的实例。一旦单独使用该方法,可能会报错。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`)
}
print(text) {
console.log(text)
}
}
class logger = new Logger()
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
单独使用, this 实际指向的是undefined,找不到print 方法。
在构造函数中绑定this, 这样就不会找不到print 方法。
class Logger {
constructor() {
this.printName = this.printName.bind(this)
}
printName(name = 'there') {
this.print(`Hello ${name}`)
}
print(text) {
console.log(text)
}
}
class logger = new Logger()
const { printName } = logger;
printName() // hello world
使用箭头函数
class Obj {
constructor() {
this.getThis = () => this
}
}
const myObj = new Obj()
getObj.getThis() ==== myObj // true
使用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 { printName } = selfish(new Logger()) // Hello world
静态方法
在类的方法上加上 static 关键字,就称为静态方法。静态方法不会被实例继承,而是通过类来调用。
class Foo {
static classMethod() {
return 'hello'
}
}
Foo.classMethod() // hello
let foo = new Foo()
foo.classMethod() // TypeError: foo.classMethod is not a function
在实例上调用静态方法,会抛出一个错误。
静态方法的 this 关键字,指的是类, 而不是实例。
class Foo {
static bar() {
this.baz()
}
static baz() {
console.log('This is static baz')
}
baz() {
console.log('world')
}
}
Foo.bar() // this is static bar
静态方法可以与非静态方法重名。
父类的静态方法可以被子类的继承。
clas Foo {
static clsMethod() {
return 'hello'
}
}
class Bar extends Foo {}
Bar.cleMethod() // 'hello'
静态方法可以从super 对象上调用
class Foo {
static clsMethod() {
return 'hello'
}
}
class Bar extends Foo {
static clsMethod() {
return super.clsMethod() + ', too!'
}
}
Bar.clsMethod() // 'hello, too!'
实例属性新写法
将这个属性定义在类的最顶层, 就不用定义在 constructor()里。
class Foo {
_count = 0;
get value() {
return this._count;
}
addCount() {
this._count++
}
}
整齐,清晰易读。
静态属性
静态属性是类本身的属性,Class.name, 而不是实例对象(this)上的属性。
// 普通写法
class Foo {}
Foo.prop = 1
Foo.prop // 1
// 静态属性
class Foo {
static myStaticProp = 42
}
Foo.myStaticProp // 42
静态属性 语义更好,更清晰。
在实例上使用 静态属性,返回 undefined.
私有方法 解决方法
私有属性和私有方法, 是只能在类的内部访问的属性和方法,外部不能访问。利于代码的封装。
命名区分(模拟)
class Widget {
// 公有
foo(baz) {
this._bar(baz)
}
// 私有
_bar(baz) {
return this.snaf = baz
}
}
不保险,在类的外部,仍可以用这个方法。
将私有方法移除模块
class Widget {
foo(baz) {
bar.call(this, baz)
}
}
function bar(baz) {
return this.snaf = baz
}
私有方法 => symbol
利用 symbol 值的唯一性,将私有方法的名字命名为一个 Symbol 值。
const bar = Symbol('bar')
const snaf = Symbol('snaf')
export default class myClass {
// 公有
foo(baz) {
this[bar](baz)
}
// 私有
[bar](baz) {
return this[snaf] = baz
}
}
Reflect.ownKeys(myClass.prototype) // ["constructor", "foo", Symbol(bar)]
一般情况无法获取 bar 和 snaf ,但 Reflect.ownKeys() 依然可以拿到它们。
私有属性的提案
在属性名前,使用#表示
class Foo {
#count = 0;
get value() {
return this.#count
}
increment() {
this.#count++
}
}
私有属性只能在类的内部使用,如果在类的外部使用就会报错。
new.target 属性
new.target属性一般用在构造函数中,返回new命令作用的那个构造函数。
如果不是通过 new 命令或 Reflect.construct() 调用,new.target 会返回undefined, 因此可用这个属性来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name
} else {
thorw new Error('必须使用new 命令生成实例')
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name
} else {
throw new Error('必须使用 new 命令生成实例')
}
}
let person = new Person('张三') // ok
let notAPerson = Person.call(pserson, '张三') // 报错
上面代码确保构造函数只能通过new命令调用。
Class 内部调用 new.target, 返回当前 Class
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
let obj = new Rectangle(3, 4) // true
子类继承父类时, new.target 会返回子类
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, width)
}
}
let obj = new Square(3); // false
利用这个属性,可以写出不能对立使用、必须继承后才能使用的类。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类本能实例化')
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super(length, width);
}
}
let x = new Shape() // 报错
let y = new Rectangle(3, 4) // ok
Shape 不能实例化,只能用于继承。链接