目錄
- 簡介
- 靜態(tài)方法
- 實例屬性的新寫法
- 靜態(tài)屬性
- 私有方法和私有屬性
-
new.target
屬性
一柜去、簡介
類的由來
ES6以前的生成實例對象的傳統(tǒng)方法是通過構(gòu)造函數(shù):
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);
上面這種寫法與傳統(tǒng)的面向?qū)ο笳Z言(比如 C++ 和 Java)差異很大件甥,所以ES6中引入了Class
關(guān)鍵字籍救,可以用來定義類,但是其大部分功能都可以用ES5實現(xiàn)晋柱,其更像一個語法糖蛇损。新的Class
寫法只是讓對象原型的寫法更加清晰、更像面向?qū)ο缶幊痰恼Z法而已挑势。
上面的例子用ES6改寫成如下:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
ES5 的構(gòu)造函數(shù)Point
,對應(yīng) ES6 的Point
類的構(gòu)造方法啦鸣。
ES6 的類潮饱,完全可以看作構(gòu)造函數(shù)的另一種寫法:
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代碼說明,類的數(shù)據(jù)類型就是函數(shù)诫给,類本身就指向構(gòu)造函數(shù)香拉。
使用的時候也是直接new
一下,和構(gòu)造函數(shù)的用法完全一致:
class Bar {
doStuff() {
console.log('stuff');
}
}
var b = new Bar();
b.doStuff() // "stuff"
構(gòu)造函數(shù)的prototype
屬性中狂,在 ES6 的“類”上面繼續(xù)存在凫碌。事實上,類的所有方法都定義在類的prototype
屬性上面:
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
在類的實例上面調(diào)用方法胃榕,其實就是調(diào)用原型上的方法:
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true
由于類的方法都定義在prototype
對象上面盛险,所以類的新方法可以添加在prototype
對象上面。Object.assign
方法可以很方便地一次向類添加多個方法:
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
prototype
對象的constructor
屬性勋又,直接指向“類”的本身苦掘,這與 ES5 的行為是一致的。
Point.prototype.constructor === Point // true
- ES6中類的內(nèi)部所有定義的方法赐写,都是不可枚舉的(non-enumerable):
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
- 但是ES5中是可以枚舉的:
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
constructor 方法
constructor
方法是類的默認方法鸟蜡,通過new
命令生成對象實例時,自動調(diào)用該方法挺邀。一個類必須有constructor
方法揉忘,如果沒有顯式定義,一個空的constructor方法會被自動添加:
class Point {
}
// 等同于
class Point {
constructor() {}
}
constructor
方法默認返回實例對象(即this)端铛,完全可以指定返回另外一個對象:
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
上面代碼中泣矛,constructor
函數(shù)返回一個全新的對象,結(jié)果導(dǎo)致實例對象不是Foo
類的實例禾蚕。
類必須使用new
調(diào)用您朽,否則會報錯。這是它跟普通構(gòu)造函數(shù)的一個主要區(qū)別换淆,后者不用new
也可以執(zhí)行:
class Foo {
constructor() {
return Object.create(null);
}
}
Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
類的實例
必須使用new
實例化:
class Point {
// ...
}
// 報錯
var point = Point(2, 3);
// 正確
var point = new Point(2, 3);
與 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
與 ES5 一樣倍试,類的所有實例共享一個原型對象:
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
p1
和p2
都是Point
的實例讯屈,它們的原型都是Point.prototype
,所以__proto__
屬性是相等的县习,這也意味著涮母,可以通過實例的__proto__
屬性為“類”添加方法谆趾。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
使用實例的__proto__
屬性改寫原型,必須相當(dāng)謹慎叛本,不推薦使用沪蓬,因為這會改變“類”的原始定義,影響到所有實例来候。
取值函數(shù)(getter)和存值函數(shù)(setter)
與 ES5 一樣跷叉,在“類”的內(nèi)部可以使用get
和set
關(guān)鍵字,對某個屬性設(shè)置存值函數(shù)和取值函數(shù)营搅,攔截該屬性的存取行為:
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'
存值函數(shù)和取值函數(shù)是設(shè)置在屬性的 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
屬性表達式
類的屬性名查排,可以采用表達式:
let methodName = 'getArea';
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
Class 表達式
與函數(shù)一樣城榛,類也可以使用表達式的形式定義:
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
需要注意的是感混,這個類的名字是Me
奇唤,但是Me
只在Class
的內(nèi)部可用,指代當(dāng)前類峭拘。在 Class
外部俊庇,這個類只能用MyClass
引用:
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
如果類的內(nèi)部沒用到的話,可以省略Me
鸡挠,也就是可以寫成下面的形式:
const MyClass = class { /* ... */ };
采用 Class 表達式辉饱,可以寫出立即執(zhí)行的 Class:
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('張三');
person.sayName(); // "張三"
注意
1. 嚴格模式
類和模塊的內(nèi)部,默認就是嚴格模式拣展,所以不需要使用use strict指定運行模式彭沼,ES6 實際上把整個語言升級到了嚴格模式。
2. 不存在變量提升
與ES5不同备埃,類不存在變量提升(hoist):
new Foo(); // ReferenceError
class Foo {}
上面代碼中姓惑,Foo
類使用在前,定義在后按脚,這樣會報錯于毙,因為 ES6 不會把類的聲明提升到代碼頭部。
{
let Foo = class {};
class Bar extends Foo {
}
}
上面的代碼不會報錯辅搬,因為Bar
繼承Foo
的時候唯沮,Foo
已經(jīng)有定義了。但是堪遂,如果存在class
的提升介蛉,上面代碼就會報錯,因為class
會被提升到代碼頭部溶褪,而let
命令是不提升的币旧,所以導(dǎo)致Bar
繼承Foo
的時候,Foo
還沒有定義竿滨。
name 屬性
由于本質(zhì)上佳恬,ES6 的類只是 ES5 的構(gòu)造函數(shù)的一層包裝,所以函數(shù)的許多特性都被Class繼承于游,包括name屬性:
class Point {}
Point.name // "Point"
name
屬性總是返回緊跟在class
關(guān)鍵字后面的類名毁葱。
Generator 方法
如果某個方法之前加上星號(*),就表示該方法是一個 Generator
函數(shù):
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
上面代碼中贰剥,Foo
類的Symbol.iterator
方法前有一個星號倾剿,表示該方法是一個 Generator
函數(shù)。Symbol.iterator
方法返回一個Foo
類的默認遍歷器蚌成,for...of
循環(huán)會自動調(diào)用這個遍歷器前痘。
this指向
類的方法內(nèi)部如果含有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
,默認指向Logger
類的實例瓶盛。但是最欠,如果將這個方法提取出來單獨使用,this
會指向該方法運行時所在的環(huán)境(由于 class
內(nèi)部是嚴格模式惩猫,所以 this
實際指向的是undefined
)芝硬,從而導(dǎo)致找不到print
方法而報錯。
可以在構(gòu)造方法中綁定this
來解決:
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
還可以使用箭頭函數(shù)來解決:
class Obj {
constructor() {
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myObj // true
箭頭函數(shù)內(nèi)部的this
總是指向定義時所在的對象轧房。上面代碼中拌阴,箭頭函數(shù)位于構(gòu)造函數(shù)內(nèi)部,它的定義生效的時候奶镶,是在構(gòu)造函數(shù)執(zhí)行的時候迟赃。
還有一種是使用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());
二捺氢、靜態(tài)方法
類相當(dāng)于實例的原型,所有在類中定義的方法剪撬,都會被實例繼承摄乒。如果在一個方法前,加上static
關(guān)鍵字残黑,就表示該方法不會被實例繼承馍佑,而是直接通過類來調(diào)用,這就稱為“靜態(tài)方法”梨水。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
靜態(tài)方法只能在類上面調(diào)用拭荤,而不能在實例上調(diào)用。
如果靜態(tài)方法包含this
關(guān)鍵字疫诽,這個this
指的是類舅世,而不是實例:
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
從上面可以看出靜態(tài)方法可以與非靜態(tài)方法重名旦委。
父類的靜態(tài)方法,可以被子類繼承:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
靜態(tài)方法也是可以從super
對象上調(diào)用:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
三雏亚、實例屬性的新寫法
定義在constructor
里:
class IncreasingCounter {
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
定義在頂部:
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
class foo {
bar = 'hello';
baz = 'world';
constructor() {
// ...
}
}
優(yōu)點是不需要寫this
缨硝,簡單明了,一目了然罢低。
四查辩、靜態(tài)屬性
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
上面的寫法為Foo
類定義了一個靜態(tài)屬性prop
。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
// 老寫法
class Foo {
// ...
}
Foo.prop = 1;
// 新寫法
class Foo {
static prop = 1;
}
五网持、私有方法和私有屬性
現(xiàn)有方案
ES6 不提供私有方法和私有屬性宜岛,只能通過變通方法模擬實現(xiàn),一種做法是在命名上加以區(qū)別功舀。
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
另一種方法就是索性將私有方法移出模塊萍倡,因為模塊內(nèi)部的所有方法都是對外可見的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
還有一種方法是利用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;
}
// ...
};
上面代碼中,bar
和snaf
都是Symbo
l值莉擒,一般情況下無法獲取到它們酿炸,因此達到了私有方法和私有屬性的效果。但是也不是絕對不行涨冀,Reflect.ownKeys()
依然可以拿到它們填硕。
const inst = new myClass();
Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]
六、new.target 屬性
new
是從構(gòu)造函數(shù)生成實例對象的命令鹿鳖。ES6 為new
命令引入了一個new.target
屬性扁眯,該屬性一般用在構(gòu)造函數(shù)之中,返回new
命令作用于的那個構(gòu)造函數(shù)翅帜。如果構(gòu)造函數(shù)不是通過new
命令或Reflect.construct()
調(diào)用的姻檀,new.target
會返回undefined
,因此這個屬性可以用來確定構(gòu)造函數(shù)是怎么調(diào)用的涝滴。
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 命令生成實例');
}
}
Class
內(nèi)部調(diào)用new.target
绣版,返回當(dāng)前 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, width);
}
}
var obj = new Square(3); // 輸出 false
上面代碼中韩脏,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
類不能被實例化,只能用于繼承弧械。
注意八酒,在函數(shù)外部,使用new.target
會報錯梦谜。