基本語法
簡介
JavaScript語言中,生成實例對象的傳統(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);
ES6提供更接近傳統(tǒng)語言的寫法,引入了Class(類)這個概念,作為對象的模板.通過class
關(guān)鍵字,可以定義類.
基本上,ES6的class
可以看做只是一個語法糖,只是讓對象原型的寫法更加清晰,更像面向?qū)ο缶幊痰恼Z法而已.上面的代碼用class
改寫,如下:
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
constructor
方法就是構(gòu)造方法,而this
關(guān)鍵字則代表實例對象.
注意,定義"類"的方法的時候,前面不需要加上function
這個關(guān)鍵字,直接把函數(shù)定義放進去就可以.另外,方法之間不需要逗號隔開,加了反而報錯.
ES6的類,完全可以看做構(gòu)造函數(shù)的另一種寫法.累的數(shù)據(jù)類型就是函數(shù),類本身就指向構(gòu)造函數(shù).
構(gòu)造函數(shù)的prototype
屬性,在ES6的"類"上面繼續(xù)存在.事實上,類的所有方法都定義在類的"prototype"屬性上面.
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
在類的實例上調(diào)用方法,其實就是調(diào)用原型上的方法.
Objet.assgin
方法可以很方便的一次向類添加多個方法.
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
另外,類的內(nèi)部所有定義的方法,都是不可枚舉的.
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
采用ES5的寫法,toString
方法就是可枚舉的.
類的屬性名,可以采用表達式.
嚴格模式
類和模塊的內(nèi)部,默認就是嚴格模式.只要你的代碼寫在類或模塊之中,就只有嚴格模式可用.
constructor方法
construtor
方法是類的默認方法,通過new
命令生成對象實例時,自動調(diào)用該方法.一個類必須有constructor
方法,如果沒有顯示定義,一個空的constructor
方法會被默認添加.
constructor
方法默認返回實例對象(即this),完全可以指定返回另外一個對象.
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
類必須使用new
調(diào)用,而普通構(gòu)造函數(shù)不用new
也可以執(zhí)行.
類的實例對象
與ES5一樣,實例的屬性除非顯示定義在其本身(即定義在this對象上),否則都是定義在原型上(即定義在class上)
類的所有實例共享一個原型對象,這就意味著,可以通過實例的__proto__
屬性為"類"添加方法.
__protp__
并不是語言本身的特性,是各大廠商具體實現(xiàn)時添加的私有屬性,不建議生產(chǎn)使用該屬性,避免對環(huán)境產(chǎn)生依賴.生產(chǎn)環(huán)境中,可以使用Object.getPrototypeOf
方法來獲取實例對象的原型,然后再來為原型添加方法/屬性.
Class表達式
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
注意:這個類的名字是MyClass
而不是Me
,Me
只在Class的內(nèi)部代碼可用,指代當前類.
如果類的內(nèi)部沒有用到的話,可以省略Me
.
采用Class表達式,可以寫出立即執(zhí)行的Class
不存在變量提升
ES6不會把類的聲明提升到代碼頭部,這種規(guī)定的愿意與繼承有關(guān),必須保證子類在父類之后定義.
私有方法和私有屬性
現(xiàn)有方法
ES6不提供私有方法這個常見需求,變通方法:
1.在命名上加以區(qū)別
2.將私有方法移除模塊,因為模塊內(nèi)部的所有方法都是對外可見的
3.利用Symbol
值的唯一性,將私有方法的名字命名為一個Symbol值
私有方案的提案
目前有一個提案,為class
加了私有屬性,方法是在屬性名之前,使用#
表示.
class Point {
#x;
constructor(x = 0) {
#x = +x; // 寫成 this.#x 亦可
}
get x() { return #x }
set x(value) { #x = +value }
}
#x
就表示私有屬性x
,在Point類之外是讀取不到這個屬性的.而且私有屬性與實例的屬性是可以同名的.
另外,私有屬性也可以設(shè)置getter和setter方法.
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
如果將這個方法提取出來單獨使用,this
會指向該方法運行時所在的環(huán)境.
一個比較簡單的解決方法是,在構(gòu)造方法中綁定this
.
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
另一種解決方法是試用箭頭函數(shù)
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
// ...
}
還有一種解決方法就是使用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());
name屬性
name屬性總是返回緊跟在class
關(guān)鍵字后面的類名
Class的取值函數(shù)(getter)和存執(zhí)函數(shù)(setter)
在"類"的內(nèi)部可以使用get
和set
關(guān)鍵字,對某個屬性設(shè)置存值函數(shù)和取值函數(shù),攔截該屬性的存取行為.
存值函數(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
Class的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
Symbol.iterator
方法返回一個Foo
類的默認遍歷器,for...of
循環(huán)會自動調(diào)用這個遍歷器.
Class的靜態(tài)方法
類相當于實例的原型,所有在類中定義的方法,都會被實例繼承.如果在一個方法前,加上static
關(guān)鍵字,就表示該方法不會被實例繼承,而是直接通過類來調(diào)用,這就成為"靜態(tài)方法".
注意:如果靜態(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)方法,可以被子類繼承,靜態(tài)方法也是可以從super
對象上調(diào)用的.
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
//從super對象上調(diào)用
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
Class的靜態(tài)屬性和實例屬性
靜態(tài)屬性指的是Class本身的屬性,即Class.propName
,而不是定義在實例對象this
上的屬性.
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
為Foo類定義了一個靜態(tài)屬性prop
目前,只有這種方法可行,因為ES6明確規(guī)定,Class內(nèi)部只有靜態(tài)方法,沒有靜態(tài)屬性.
目前有一個關(guān)于靜態(tài)屬性的提案,對實例屬性和靜態(tài)屬性都規(guī)定了新的寫法:
1.類的實例屬性
類的實例屬性可以用等式,寫入類的定義之中
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
以前我們定義實例屬性,只能寫在類的constructor
方法里面.
class ReactCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
}
為了可讀性的目的,對于那些在construtor
里面已經(jīng)定義的實例屬性,新鞋發(fā)允許直接列出.
2.類的靜態(tài)屬性
只要在上面的實例寫法前面加上static
關(guān)鍵字就可以了
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
new.target屬性
new
是從構(gòu)造函數(shù)生成實例對象的命令.ES6為new
命令引入一個new.target
屬性,該屬性一般用在構(gòu)造函數(shù)之中,返回new
命令作用域的那個構(gòu)造函數(shù).如果構(gòu)造函數(shù)不是通過new
命令調(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 命令生成實例');
}
}
var person = new Person('張三'); // 正確
var notAPerson = Person.call(person, '張三'); // 報錯
Class內(nèi)部調(diào)用new.target
返回當前Class.需要注意的是,子類繼承父類時,new.target
會返回子類.
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var obj = new Square(3); // 輸出 false
利用這個特點,可以寫出不能獨立使用,必須繼承后才能使用的類.
class的繼承
基本用法
Class可以通過extends
關(guān)鍵字實現(xiàn)繼承,ES5是通過修改原型鏈實現(xiàn)繼承的
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 調(diào)用父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 調(diào)用父類的toString()
}
}
constructor
方法和toString
方法之中,都出現(xiàn)了super
關(guān)鍵字,它在這里表示父類的構(gòu)造函數(shù),用來新建父類的this
對象.
子類必須在constructor
方法中調(diào)用super
方法,否則新建實例時會報錯.這是因為子類沒有自己的this
對象,而是繼承父類的this
對象,然后對其進行加工.如果不調(diào)用super
方法,子類就得不到this
對象.
ES5的繼承實質(zhì)是先創(chuàng)造子類的實例對象this
,然后再將父類的方法添加到this
上面(Parent.apply(this)).ES6的繼承機制完全不同,實質(zhì)是先創(chuàng)造父類的實例對象this
(所以必須先調(diào)用super
方法),然后再用子類的構(gòu)造函數(shù)修改this
.
如果子類沒有定義constructor
方法,這個方法會被默認添加.
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
特別注意:在子類的構(gòu)造函數(shù)總,只有調(diào)用super
之后,才可以使用this
關(guān)鍵字,否則會報錯.
通過子類生成實例的對象同時是父類和子類的實例.
父類的靜態(tài)方法也會被子類繼承.
Object.getPrototypeOf()
該方法可以用來從子類上獲取父類,可以用這個方法判斷,一個類是否繼承了另一個類.
super關(guān)鍵字
super
關(guān)鍵字既可以當做函數(shù)使用,也可以當做對象使用.
第一種情況,super
作為函數(shù)調(diào)用時,代表父類的構(gòu)造函數(shù).ES6要求,子類的構(gòu)造函數(shù)必須執(zhí)行一次super
函數(shù).
注意.super
雖然代表了父類的構(gòu)造函數(shù),但是返回的是子類的實例.即super
內(nèi)部的this
指向的是子類,因此super()
在這里相當于父類.prototype.constructor.call(this)
.
作為函數(shù)時,super()
只能用在子類的構(gòu)造函數(shù)中.
第二種情況,super
作為對象時,在普通方法總,指向父類的原型兌現(xiàn);在靜態(tài)方法中,指向父類.
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
這里需要注意,由于super
指向父類的原型對象,所以定義在父類實例上的方法或?qū)傩?是無法通過super
調(diào)用的.
在子類普通方法中通過super
調(diào)用父類的方法時,方法內(nèi)部的this
指向當前的子類實例.
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
由于this
指向子類實例,所以如果通過super
對某個屬性賦值,這是super
就是this
,賦值的舒心會變成子類實例的屬性.
如果super
作為對象用在靜態(tài)方法中,這時super
將指向父類,而不是父類的原型對象.
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
在子類的靜態(tài)方法中通過super
調(diào)用父類的方法時,方法內(nèi)部的this
指向當前的子類,而不是子類的實例.
注意,使用super
的時候,必須顯示指定是作為函數(shù),還是作為對象使用,否則會報錯.
由于對象總是繼承其它對象的,所以可以在任意一個對象中,使用super
關(guān)鍵字.
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
類的prototype屬性和proto屬性
大多數(shù)瀏覽器的ES5實現(xiàn)中,每一個對象都有__proto__
屬性,指向?qū)?yīng)的構(gòu)造函數(shù)的prototype
屬性.Class作為構(gòu)造函數(shù)的語法糖,同時有prototype
屬性和__proto__
屬性,因此同時存在兩條繼承鏈.
1.子類的__proto__
屬性,表示構(gòu)造函數(shù)的繼承,總是指向父類.
2.子類prototype
屬性的__proto__
屬性表示方法的繼承,總是指向父類的prototype
屬性.
extends的繼承目標
extends
關(guān)鍵字后面可以跟多種類型的值.
只要是一個有prototype
屬性的函數(shù),就能被B繼承.由于函數(shù)都有prototype
屬性(除了Function.prototype函數(shù)),因此被繼承的可以是任意函數(shù).
三種特殊情況:
- 繼承
Object
類
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
這種情況下,A其實就是構(gòu)造函數(shù)Object的復(fù)制,A的實例就是Object的實例.
- 不存在任何繼承
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
這種情況下,A作為一個基類(不存在任何繼承),就是一個普通函數(shù),所以直接繼承Function.prototype
.但是,A調(diào)用后返回一個空對象,所以A.prototype.__proto__
指向構(gòu)造函數(shù)(Object)的prototype
屬性.
- 子類繼承null
class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
與第二種情況類似.A也是一個普通函數(shù),所以直接繼承Funciotn.prototype
.但是,A調(diào)用后返回的對象不繼承任何方法,所以他的__proto__
指向Function.prototype
,即實質(zhì)執(zhí)行了下面代碼:
class C extends null {
constructor() { return Object.create(null); }
}
實例的__proto__
屬性
子類實例的__proto__
屬性的__proto__
屬性,指向父類實例的__proto__
屬性.子類的原型的原型就是父類的原型.
原生構(gòu)造函數(shù)的繼承
ECMAScript的原生構(gòu)造函數(shù)大致有:
- Boolean
- Number
- String
- Array
- Date
- Function
- RegExp
- Error
- Object
以前這些原生構(gòu)造函數(shù)無法繼承,之所以不能繼承,是因為子類無法獲得原生構(gòu)造函數(shù)的內(nèi)部屬性,通過apply()
或者分配給原型對象都不行.原生構(gòu)造函數(shù)會忽略apply
方法傳入的this
,即原生構(gòu)造函數(shù)的this
無法綁定,導(dǎo)致拿不到內(nèi)部屬性.
ES5是先新建子類的實例對象this
,再將父類的屬性添加到子類上,由于子類的內(nèi)部屬性無法獲取,導(dǎo)致無法繼承原生的構(gòu)造函數(shù).比如,Array
構(gòu)造函數(shù)有一個內(nèi)部屬性[[DefineOenProperty]]
,用來定義新屬性時,更新length
屬性,這個內(nèi)部屬性無法在子類獲取,導(dǎo)致子類的length
屬性行為不正常.
ES6允許集成原生構(gòu)造函數(shù)定義子類,因為ES6是先新建父類的實例對象this
,然后再用子類的構(gòu)造函數(shù)修飾this
,使得父類的所有行為都可以繼承.這意味著,ES6可以自定義原生數(shù)據(jù)結(jié)構(gòu)的子類.
extends
關(guān)鍵字不僅可以用來繼承類,還可以用來繼承原生的構(gòu)造函數(shù).因此可以再原生數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ)上,定義自己的數(shù)據(jù)結(jié)構(gòu).
注意,繼承Object
的子類,有一個行為差異
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
NewObj繼承了Object,但是無法通過super
方法向父類傳參.這是因為ES6改變了Object
構(gòu)造函數(shù)的行為,一旦發(fā)現(xiàn)Obejct
方法不是通過new Object()
這種形式調(diào)用,ES6規(guī)定Object
構(gòu)造函數(shù)會忽略參數(shù).
Mixin模式的實現(xiàn)
Mixin指的是多個對象合成一個新的對象,新對象具有各個組成成員的接口.最簡單實現(xiàn)為:
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
更完備的實現(xiàn),將多個類的接口"混入"(mix in)另一個類
function mix(...mixins) {
class Mix {}
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);
}
}
}
上面代碼的mix
函數(shù),可以將多個對象合成一個類.使用的時候,只要繼承這個類即可.
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}