ES6(簡介及常用)-上
一、類的支持
1、簡介
ES6中添加了對類的支持,引入了class關鍵字。JS本身就是面向對象的,ES6中提供的類實際上只是JS原型模式的包裝。現在提供原生的class支持後,對象的創建,繼承更加直觀了,並且父類方法的調用,實例化,靜態方法和構造函數等概念都更加形象化。JavaScript 語言中,生成實例對象的傳統方法是通過構造函數。下面是一個例子。
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 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為對象的模板。通過class關鍵字,可以定義類。基本上,ES6 的class可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼用 ES6 的class改寫,就是下面這樣。
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘;
}
}
上面代碼定義了一個“類”,可以看到裏面有一個constructor方法,這就是構造方法,而this關鍵字則代表實例對象。也就是說,ES5 的構造函數Point,對應 ES6 的Point類的構造方法。Point類除了構造方法,還定義了一個toString方法。註意,定義“類”的方法的時候,前面不需要加上function這個關鍵字,直接把函數定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。ES6 的類,完全可以看作構造函數的另一種寫法。使用的時候,也是直接對類使用new命令,跟構造函數的用法完全一致。
class Bar {
doStuff() {
console.log(‘stuff‘);
}
}
var b = new Bar();
b.doStuff() // "stuff"
prototype 屬性使您有能力向對象添加屬性和方法。(object.prototype.name=value)構造函數的prototype屬性,在 ES6 的“類”上面繼續存在。事實上,類的所有方法都定義在類的prototype屬性上面。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同於
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
由於類的方法都定義在prototype對象上面,所以類的新方法可以添加在prototype對象上面。Object.assign方法可以很方便地一次向類添加多個方法。
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
prototype對象的constructor屬性,直接指向“類”的本身,這與 ES5 的行為是一致的。
Point.prototype.constructor === Point // true
2、類的實例對象
生成類的實例對象的寫法,與 ES5 完全一樣,也是使用new命令。前面說過,如果忘記加上new,像函數那樣調用Class,將會報錯。
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
上面代碼中,x和y都是實例對象point自身的屬性(因為定義在this變量上),所以hasOwnProperty方法返回true,而toString是原型對象的屬性(因為定義在Point類上),所以hasOwnProperty方法返回false。這些都與 ES5 的行為保持一致。與 ES5 一樣,類的所有實例共享一個原型對象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
上面代碼中,p1和p2都是Point的實例,它們的原型都是Point.prototype,所以__proto__屬性是相等的。
3、Class 表達式
與函數一樣,類也可以使用表達式的形式定義。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代碼使用表達式定義了一個類。需要註意的是,這個類的名字是MyClass而不是Me,Me只在 Class 的內部代碼可用,指代當前類。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代碼表示,Me只在 Class 內部有定義。如果類的內部沒用到的話,可以省略Me,也就是可以寫成下面的形式。
const MyClass = class { /* ... */ };
采用 Class 表達式,可以寫出立即執行的 Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}(‘張三‘);
person.sayName(); // "張三"
4、不存在變量提升
類不存在變量提升(hoist),這一點與 ES5 完全不同。
new Foo(); // ReferenceError
class Foo {}
上面代碼中,Foo類使用在前,定義在後,這樣會報錯,因為 ES6 不會把類的聲明提升到代碼頭部。這種規定的原因與下文要提到的繼承有關,必須保證子類在父類之後定義。
{
let Foo = class {};
class Bar extends Foo {
}
}
上面的代碼不會報錯,因為Bar繼承Foo的時候,Foo已經有定義了。但是,如果存在class的提升,上面代碼就會報錯,因為class會被提升到代碼頭部,而let命令是不提升的,所以導致Bar繼承Foo的時候,Foo還沒有定義。
5、私有方法
私有方法是常見需求,但 ES6 不提供,只能通過變通方法模擬實現。
一種做法是在命名上加以區別。
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
上面代碼中,_bar方法前面的下劃線,表示這是一個只限於內部使用的私有方法。但是,這種命名是不保險的,在類的外部,還是可以調用到這個方法。
另一種方法就是索性將私有方法移出模塊,因為模塊內部的所有方法都是對外可見的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
上面代碼中,foo是公有方法,內部調用了bar.call(this, baz)。這使得bar實際上成為了當前模塊的私有方法。
還有一種方法是利用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都是Symbol值,導致第三方無法獲取到它們,因此達到了私有方法和私有屬性的效果。
6、私有屬性
與私有方法一樣,ES6 不支持私有屬性。目前,有一個提案,為class加了私有屬性。方法是在屬性名之前,使用#表示。
class Point {
#x;
constructor(x = 0) {
#x = +x; // 寫成 this.#x 亦可
}
get x() { return #x }
set x(value) { #x = +value }
}
上面代碼中,#x就表示私有屬性x,在Point類之外是讀取不到這個屬性的。還可以看到,私有屬性與實例的屬性是可以同名的(比如,#x與get x())。
私有屬性可以指定初始值,在構造函數執行時進行初始化。
class Point {
#x = 0;
constructor() {
#x; // 0
}
}
之所以要引入一個新的前綴#表示私有屬性,而沒有采用private關鍵字,是因為 JavaScript 是一門動態語言,使用獨立的符號似乎是唯一的可靠方法,能夠準確地區分一種屬性是否為私有屬性。另外,Ruby 語言使用@表示私有屬性,ES6 沒有用這個符號而使用#,是因為@已經被留給了 Decorator。
還有其他屬性:
Class 的取值函數(getter)和存值函數(setter)
Class 的 Generator 方法
Class 的靜態方法
2、簡介( class的繼承 )
Class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
class Point {
}
class ColorPoint extends Point {
}
上面代碼定義了一個ColorPoint類,該類通過extends關鍵字,繼承了Point類的所有屬性和方法。但是由於沒有部署任何代碼,所以這兩個類完全一樣,等於復制了一個Point類。下面,我們在ColorPoint內部加上代碼。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 調用父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ‘ ‘ + super.toString(); // 調用父類的toString()
}
}
上面代碼中,constructor方法和toString方法之中,都出現了super關鍵字,它在這裏表示父類的構造函數,用來新建父類的this對象。子類必須在constructor方法中調用super方法,否則新建實例時會報錯。這是因為子類沒有自己的this對象,而是繼承父類的this對象,然後對其進行加工。如果不調用super方法,子類就得不到this對象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
上面代碼中,ColorPoint繼承了父類Point,但是它的構造函數沒有調用super方法,導致新建實例時報錯。ES5 的繼承,實質是先創造子類的實例對象this,然後再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機制完全不同,實質是先創造父類的實例對象this(所以必須先調用super方法),然後再用子類的構造函數修改this。
如果子類沒有定義constructor方法,這個方法會被默認添加,代碼如下。也就是說,不管有沒有顯式定義,任何一個子類都有constructor方法。
class ColorPoint extends Point {
}
// 等同於
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
另一個需要註意的地方是,在子類的構造函數中,只有調用super之後,才可以使用this關鍵字,否則會報錯。這是因為子類實例的構建,是基於對父類實例加工,只有super方法才能返回父類實例。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正確
}
}
上面代碼中,子類的constructor方法沒有調用super之前,就使用this關鍵字,結果報錯,而放在super方法之後就是正確的。
下面是生成子類實例的代碼。
let cp = new ColorPoint(25, 8, ‘green‘);
cp instanceof ColorPoint // true
cp instanceof Point // true
上面代碼中,實例對象cp同時是ColorPoint和Point兩個類的實例,這與 ES5 的行為完全一致。
3、Object.getPrototypeOf()
Object.getPrototypeOf方法可以用來從子類上獲取父類。
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用這個方法判斷,一個類是否繼承了另一個類。
4、super 關鍵字
super這個關鍵字,既可以當作函數使用,也可以當作對象使用。在這兩種情況下,它的用法完全不同。
第一種情況,super作為函數調用時,代表父類的構造函數。ES6 要求,子類的構造函數必須執行一次super函數。
class A {}
class B extends A {
constructor() {
super();
}
}
上面代碼中,子類B的構造函數之中的super(),代表調用父類的構造函數。這是必須的,否則 JavaScript 引擎會報錯。
註意,super雖然代表了父類A的構造函數,但是返回的是子類B的實例,即super內部的this指的是B。
作為函數時,super()只能用在子類的構造函數之中,用在其他地方就會報錯。
class A {}
class B extends A {
m() {
super(); // 報錯
}
}
第二種情況,super作為對象時,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
上面代碼中,子類B當中的super.p(),就是將super當作一個對象使用。這時,super在普通方法之中,指向A.prototype,所以super.p()就相當於A.prototype.p()。
這裏需要註意,由於super指向父類的原型對象,所以定義在父類實例上的方法或屬性,是無法通過super調用的。
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined
上面代碼中,p是父類A實例的屬性,super.p就引用不到它。
如果屬性定義在父類的原型對象上,super就可以取到。
class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();
上面代碼中,屬性x是定義在A.prototype上面的,所以super.x可以取到它的值。
ES6 規定,通過super調用父類的方法時,super會綁定子類的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
上面代碼中,super.print()雖然調用的是A.prototype.print(),但是A.prototype.print()會綁定子類B的this,導致輸出的是2,而不是1。也就是說,實際上執行的是super.print.call(this)。
還有:extends 的繼承目標
實例的 __proto__ 屬性(子類實例的__proto__屬性的__proto__屬性,指向父類實例的__proto__屬性。也就是說,子類的原型的原型,是父類的原型。)
原生構造函數的繼承(語言內置的構造函數,通常用來生成數據結構。)
Mixin 模式的實現(將多個類的接口“混入”(mix in)另一個類。它在 ES6 的實現如下。)
二、增強的對象字面量
對象字面量被增強了,寫法更加簡潔與靈活,同時在定義對象的時候能夠做的事情更多了。具體表現在:
- 可以在對象字面量裏面定義原型
- 定義方法可以不用function關鍵字
- 直接調用父類方法
這樣一來,對象字面量與前面提到的類概念更加吻合,在編寫面向對象的JavaScript時更加輕松方便了。
//通過對象字面量創建對象
var human = {
breathe() {
console.log(‘breathing...‘);
}
};
var worker = {
__proto__: human, //設置此對象的原型為human,相當於繼承human
company: ‘freelancer‘,
work() {
console.log(‘working...‘);
}
};
human.breathe();//輸出 ‘breathing...’
//調用繼承來的breathe方法
worker.breathe();//輸出 ‘breathing...’
三、字符串模板
字符串模板相對簡單易懂些。ES6中允許使用反引號 ` 來創建字符串,此種方法創建的字符串裏面可以包含由美元符號加花括號包裹的變量${vraible}。如果你使用過像C#等後端強類型語言的話,對此功能應該不會陌生。
//產生一個隨機數
var num=Math.random();
//將這個數字輸出到console
console.log(`your num is ${num}`);
四、解構
ES6 允許按照一定模式,從數組和對象中提取值,對變量進行賦值,這被稱為解構
以前,為變量賦值,只能直接指定值。
let a = 1;
let b = 2;
let c = 3;
ES6 允許寫成下面這樣。
let [a, b, c] = [1, 2, 3];
上面代碼表示,可以從數組中提取值,按照對應位置,對變量賦值。
本質上,這種寫法屬於“模式匹配”,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值。下面是一些使用嵌套數組進行解構的例子。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = [‘a‘];
x // "a"
y // undefined
z // []
如果解構不成功,變量的值就等於undefined。另一種情況是不完全解構,即等號左邊的模式,只匹配一部分的等號右邊的數組。這種情況下,解構依然可以成功。
如果等號的右邊不是數組(嚴格地說,不是可遍歷的結構),那麽將會報錯。
// 報錯
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
上面的語句都會報錯,因為等號右邊的值,要麽轉為對象以後不具備 Iterator 接口(前五個表達式),要麽本身就不具備 Iterator 接口(最後一個表達式)。
對於 Set 結構,也可以使用數組的解構賦值。
let [x, y, z] = new Set([‘a‘, ‘b‘, ‘c‘]);
x // "a"
事實上,只要某種數據結構具有 Iterator 接口,都可以用數組形式解構賦值。(選)默認值:解構賦值允許指定默認值。
對象的解構賦值:對象的解構與數組有一個重要的不同。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才能取到正確的值。
字符串的解構賦值:字符串被轉換成了一個類似數組的對象。
數值和布爾值的解構賦值:如果等號右邊是數值和布爾值,則會先轉為對象。只要等號右邊的值不是對象或數組,就先將其轉為對象。由於undefined和null無法轉為對象,所以對它們進行解構賦值,都會報錯。
用途:(1)交換變量的值;(2)從函數返回多個值;(3)函數參數的定義;(4)提取JSON數據;(5)函數參數的默認值;(6)遍歷map結構;(7)輸入模塊的指定方法
五、參數默認值,不定參數,拓展參數
默認參數值
現在可以在定義函數的時候指定參數的默認值了,而不用像以前那樣通過邏輯或操作符來達到目的了。
function sayHello(name){
//傳統的指定默認參數的方式
var name=name||‘dude‘;
console.log(‘Hello ‘+name);
}
//運用ES6的默認參數
function sayHello2(name=‘dude‘){
console.log(`Hello ${name}`);
}
sayHello();//輸出:Hello dude
sayHello(‘Wayou‘);//輸出:Hello Wayou
sayHello2();//輸出:Hello dude
sayHello2(‘Wayou‘);//輸出:Hello Wayou
六、不定參數
不定參數是在函數中使用命名參數同時接收不定數量的未命名參數。這只是一種語法糖,在以前的JavaScript代碼中我們可以通過arguments變量來達到這一目的。不定參數的格式是三個句點後跟代表所有不定參數的變量名。比如下面這個例子中,…x代表了所有傳入add函數的參數。
//將所有參數相加的函數
function add(...x){
return x.reduce((m,n)=>m+n);
}
//傳遞任意個數的參數
console.log(add(1,2,3));//輸出:6
console.log(add(1,2,3,4,5));//輸出:15
七、let與const 關鍵字
1、可以把let看成var,只是它定義的變量被限定在了特定範圍內才能使用,而離開這個範圍則無效,for循環的計數器,就很合適使用let命令。(例:)
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
下面的代碼如果使用var,最後輸出的是10。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代碼中,變量i是var命令聲明的,在全局範圍內都有效,所以全局只有一個變量i。每一次循環,變量i的值都會發生改變,而循環內被賦給數組a的函數內部的console.log(i),裏面的i指向的就是全局的i。也就是說,所有數組a的成員裏面的i,指向的都是同一個i,導致運行時輸出的是最後一輪的i的值,也就是10。
如果使用let,聲明的變量僅在塊級作用域內有效,最後輸出的是6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代碼中,變量i是let聲明的,當前的i只在本輪循環有效,所以每一次循環的i其實都是一個新的變量,所以最後輸出的是6。你可能會問,如果每一輪循環的變量i都是重新聲明的,那它怎麽知道上一輪循環的值,從而計算出本輪循環的值?這是因為 JavaScript 引擎內部會記住上一輪循環的值,初始化本輪的變量i時,就在上一輪循環的基礎上進行計算。
另外,for循環還有一個特別之處,就是設置循環變量的那部分是一個父作用域,而循環體內部是一個單獨的子作用域。
for (let i = 0; i < 3; i++) {
let i = ‘abc‘;
console.log(i);
}
// abc
// abc
// abc
上面代碼正確運行,輸出了3次abc。這表明函數內部的變量i與循環變量i不在同一個作用域,有各自單獨的作用域。let不允許重復聲明;不存在變量提升。
2、塊級作用域
ES5 只有全局作用域和函數作用域,沒有塊級作用域,這帶來很多不合理的場景。
第一種場景,內層變量可能會覆蓋外層變量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = ‘hello world‘;
}
}
f(); // undefined
上面代碼的原意是,if代碼塊的外部使用外層的tmp變量,內部使用內層的tmp變量。但是,函數f執行後,輸出結果為undefined,原因在於變量升,導致內層的tmp變量覆蓋了外層的tmp變量。
第二種場景,用來計數的循環變量泄露為全局變量。
var s = ‘hello‘;
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面代碼中,變量i只用來控制循環,但是循環結束後,它並沒有消失,泄露成了全局變量。
3、ES6 的塊級作用域
let實際上為 JavaScript 新增了塊級作用域。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函數有兩個代碼塊,都聲明了變量n,運行後輸出5。這表示外層代碼塊不受內層代碼塊的影響。如果兩次都使用var定義變量n,最後輸出的值才是10。
const則很直觀,用來定義常量,即無法被更改值的量。const一旦聲明變量,就必須立即初始化,不能留到以後賦值。const的作用域與let命令相同:只在聲明所在的塊級作用域內有效。const命令聲明的常量也是不提升。
4、本質
const實際上保證的,並不是變量的值不得改動,而是變量指向的那個內存地址不得改動。對於簡單類型的數據(數值、字符串、布爾值),值就保存在變量指向的那個內存地址,因此等同於常量。但對於復合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指針,const只能保證這個指針是固定的,至於它指向的數據結構是不是可變的,就完全不能控制了。因此,將一個對象聲明為常量必須非常小心。
ES6(簡介及常用)-上