提到 ECMAScript,可能很多 Web 開發人員會覺得比較陌生。但是提到 JavaScript,大家應該都比較熟悉。實際上,ECMAScript 是標准化組織 ECMA發布的腳本語言規范。現在大家常見的 JavaScript、微軟的 JScript 以及 Adobe 的 ActionScript 等語言都是遵循這個規范的,屬於 ECMAScript 語言的變體。每個 ECMAScript 規范的變體語言都可能增加自己額外的功能特性。理解 ECMAScript 規范本身,就可以對很多 JavaScript 語言中的復雜特性有比較深入的了解。
初次聽到ES5、ES6,我有點兒懵了。好吧!我承認我不是一個合格的程序員。所以,我開始重新認識ES5、ES6。
一、什么是ES?
ES全稱為:ECMAScript,是一種由Ecma國際(前身為歐洲計算機制造商協會,英文名稱是European Computer Manufacturers Association)通過ECMA-262標准化的腳本程序設計語言,至今為止有六個版本。這種語言在萬維網上應用廣泛,它往往被稱為JavaScript或JScript,但實際上后兩者是ECMA-262標准的實現和擴展。
二、ECMAScript關鍵字的完整列表:
Undefined
、Null
、Boolean
、Number
、String
和 Object
。在此,我就不對每個類型進行贅敘了。
命名屬性有自己的特性(attribute)來定義該屬性本身的行為。對於命名數據屬性來說,特性 [[Value]]
表示該屬性的值,可以是任何 ECMAScript 語言中定義的類型的值;[[Writable]]
表示該屬性的值是否為只讀的;[[Enumerable]]
表示該屬性是否可以被枚舉。可以被枚舉的屬性可以通過 for-in
循環來獲取到;[[Configurable]]
表示該屬性是否可以被配置。如果 [[Configurable]]
的值為 true
,則該屬性可以被刪除、可以被轉換為訪問器屬性、還可以改變除了 [[Value]]
之外的其他特性的值。對於命名訪問器屬性來說,這類屬性沒有[[Value]]
和 [[Writable]]
特性,取而代之的是進行屬性值獲取和設置的 [[Get]]
和 [[Set]]
特性。如果 [[Get]]
和 [[Set]]
特性的值不是 undefined
,那么就必須是一個函數。屬性的獲取和設置是通過調用這兩個函數來完成的。命名訪問器屬性同樣有[[Enumerable]]
和 [[Configurable]]
特性,其含義與命名數據屬性的對應特性的含義相同。命名屬性可以在 ECMAScript 代碼中進行處理。
內部屬性的作用是定義 ECMAScript 中的對象在不同情況下的行為。不同類型的對象所包含的內部屬性也不盡相同。每個對象都有內部屬性[[Prototype]]
,用來引用另外一個對象。被引用的對象的 [[Prototype]]
屬性又可以引用另外一個對象。對象之間通過這種引用關系組成一個鏈條,稱為原型鏈條(prototype chain)。ECMAScript 通過原型鏈條的方式來實現屬性的繼承。當嘗試獲取一個對象中的命名數據屬性時,如果在當前對象中沒有相應名稱的屬性,會沿着原型鏈條往上查找,直到找到該屬性或到達原型鏈條的末尾;當設置命名數據屬性時,如果當前對象中不存在相應名稱的屬性,會在當前對象中創建新的屬性。命名訪問器屬性則始終是繼承的。當設置一個命名訪問器屬性的值時,所設置的是原型鏈條上定義該屬性的對象上的值。
內部屬性 [[Class]]
用來聲明對象的類別,其作用類似於 Java 語言中對象的類名。通過 Object.prototype.toString
函數可以獲取到[[Class]]
屬性的值。當需要判斷一個對象是否為數組時,可以使用代碼 Object.prototype.toString.apply(obj) === '[object Array]'
。
Object.defineProperty 函數的使用示例
var obj = {}; Object.defineProperty(obj, 'val', {}); // 創建一個新屬性,特性為默認值 obj.val = 1; Object.defineProperty(obj, 'CONSTANT', {value : 32, writable : false}); // 創建一個只讀屬性 obj.CONSTANT = 16; // 對屬性的修改是無效的,但是不會拋出錯誤 Object.defineProperty(obj, "newVal", {enumerable: true}); for (var key in obj) { console.log(key); // 可以枚舉出 newVal } var initValue = 0; Object.defineProperty(obj, "initValue", { get : function() { return initValue; }, set : function(val) { if (val > 0) { initValue = val; } } });
通過賦值操作創建新屬性
var obj = {val : 1}; obj.newVal = "Hello"; Object.seal(obj); Object.defineProperty(obj, 'anotherVal', {}); // 拋出 TypeError 錯誤
數組
數組是 ECMAScript 中非常重要的一個內置對象。在 ECMAScript 代碼中可以看到大量對數組的使用。Array
對象用來表示數組。在 ECMAScript 規范第三版中並沒有為 Array
對象提供比較多的實用函數來對數組進行操作。很多 JavaScript 框架對 ECMAScript 規范中的Array
對象進行增強。ECMAScript 規范第五版中對 Array
對象進行了增強,因此很多功能可以直接依靠運行環境的實現。
Array
對象本身是一個構造函數,可以用來創建新的數組實例。當 Array
對象本身作為一個函數來使用時,其作用相當於作為構造函數來使用。因此“Array(1,2,3)
”的結果與“new Array(1,2,3)
”是相同的。新創建的 Array
對象實例的內部屬性 [[Prototype]]
的值是內置的Array
原型對象,即 Array.prototype
。通過 Array.isArray
函數可以判斷一個對象是否為數組。
Array.prototype 中函數的使用示例
var array = [1, 2, 3, 4, 5]; array.indexOf(3); // 值為 2 array.lastIndexOf(4); // 值為 3 array.every(function(value, index, arr) { return value % 2 === 0; }); // 值為 false array.some(function(value, index, arr) { return value % 2 === 0; }); // 值為 true array.forEach(function(value, index, arr) { console.log(value); }); array.map(function(value, index, arr) { return value * 2; }); // 值為 [2, 4, 6, 8, 10] array.filter(function(value, index, arr) { return value % 2 === 0; }); // 值為 [2, 4] array.reduce(function(preValue, value, index, arr) { return preValue + value; }); // 值為 15 array.reduceRight(function(preValue, value, index, arr) { return preValue * value; }); // 值為 120
實際上,Array.prototype
中的函數並不限制只能對數組對象來使用。這些函數本身是通用的。比較典型的是在函數中對 arguments
對象的處理。arguments
對象本身不是數組類型的,但是一樣可以使用 Array.prototype
的函數來進行處理。
JSON
在 ECMAScript 代碼中,經常會需要與 JSON 格式的數據進行交換。JSON 也通常被用來作為客戶端與服務器端之間的數據傳輸格式。這主要是因為在 ECMAScript 代碼中處理 JSON 格式非常自然。JSON 格式數據經過解析之后,可以直接當成 ECMAScript 中的對象來使用。在使用 JSON 格式時的一個重要問題是如何在 ECMAScript 中的對象與文本形式之間進行互相轉換。從服務器端通過 HTTP 協議獲取的 JSON 文本需要經過解析之后,才能在 ECMAScript 代碼中來使用;當需要向服務器端發送數據時,需要先把 ECMAScript 中的對象轉換成文本格式。在 ECMAScript 規范第三版中並沒有對 JSON 格式數據的轉換進行規范,大多數程序都依靠 JavaScript 框架來提供相關的支持。
JSON 對象的 parse 函數的使用示例
var jsonStr = '{"a":1, "b":2, "c":3}'; JSON.parse(jsonStr); JSON.parse(jsonStr, function(key, value) { return typeof value === 'number' ? value * 2 : value; }); // 結果為 {a:2, b:4, c:6} JSON.parse(jsonStr, function(key, value) { return typeof value === 'number' && value % 2 === 0 ? undefined : value; }); // 結果為 {a:1, b:3}
JSON 對象中 stringify 函數的使用示例
var user = { name : 'Alex', password : 'password', email : 'alex@example.org' }; JSON.stringify(user); JSON.stringify(user, ['name']); // 輸出結果為“{"name":"Alex"}” JSON.stringify(user, function(key, value) { if (key === 'email') { return '******'; } if (key === 'password') { return undefined; } return value; }); // 輸出結果為“{"name":"Alex","email":"******"}” JSON.stringify(user, null, 4);
代碼執行
ECMAScript 代碼的執行由運行環境來完成。不同的運行環境可能采取不同的執行方式,但基本的流程是相同的。如瀏覽器在解析 HTML 頁面中遇到 <script>
元素時,會下載對應的代碼來運行,或直接執行內嵌的代碼。在代碼中通過 eval
函數也可以指定一段需要執行的代碼。代碼的基本執行方式是從上到下,順序執行。在調用函數之后,代碼的執行會進入一個執行上下文之中。由於在一個函數的執行過程中會調用其他的函數,執行過程中的活動執行上下文會形成一個堆棧結構。在棧頂的是當前正在執行的代碼。當函數返回時,會退出當前的執行上下文,而回到之前的執行上下文中。如果代碼執行中出現異常,則可能從多個執行上下文中退出。
演示詞法環境的代碼示例
var name = "alex"; function outer() { var age = 30; function inner(salutation) { return "Age of " + salutation + name + " is " + age; } return inner("Mr."); } outer();
四、ES5中新增的Array方法
ES5中新增的不少東西,比如數組這塊,我們可能就不需要去有板有眼地for
循環了。
ES5中新增了寫數組方法,如下:
- forEach (js v1.6)
- map (js v1.6)
- filter (js v1.6)
- some (js v1.6)
- every (js v1.6)
- indexOf (js v1.6)
- lastIndexOf (js v1.6)
- reduce (js v1.8)
- reduceRight (js v1.8)
瀏覽器支持
- Opera 11+
- Firefox 3.6+
- Safari 5+
- Chrome 8+
- Internet Explorer 9+
對於讓人失望很多次的IE6-IE8瀏覽器,Array原型擴展可以實現以上全部功能,例如forEach
方法:
// 對於古董瀏覽器,如IE6-IE8 if (typeof Array.prototype.forEach != "function") { Array.prototype.forEach = function () { /* 實現 */ }; }
下面,我就選取其中一個方法進行示范:
forEach
forEach
是Array新方法中最基本的一個,就是遍歷,循環。例如下面這個例子:
[1, 2 ,3, 4].forEach(alert);
等同於下面這個傳統的for
循環:
var array = [1, 2, 3, 4]; for (var k = 0, length = array.length; k < length; k++) { alert(array[k]); }
Array在ES5新增的方法中,參數都是function
類型,默認有傳參,這些參數分別是?見下面:
[1, 2 ,3, 4].forEach(console.log); // 結果: // 1, 0, [1, 2, 3, 4] // 2, 1, [1, 2, 3, 4] // 3, 2, [1, 2, 3, 4] // 4, 3, [1, 2, 3, 4]
顯而易見,forEach
方法中的function
回調支持3個參數,第1個是遍歷的數組內容;第2個是對應的數組索引,第3個是數組本身。
因此,我們有:
[].forEach(function(value, index, array) { // ... });
對比jQuery中的$.each
方法:
$.each([], function(index, value, array) { // ... });
會發現,第1個和第2個參數正好是相反的,大家要注意了,不要記錯了。后面類似的方法,例如$.map
也是如此。
現在,我們就可以使用forEach
賣弄一個稍顯完整的例子了,數組求和:
var sum = 0; [1, 2, 3, 4].forEach(function (item, index, array) { console.log(array[index] == item); // true sum += item; }); alert(sum); // 10
再下面,更進一步,forEach
除了接受一個必須的回調函數參數,還可以接受一個可選的上下文參數(改變回調函數里面的this
指向)(第2個參數)。
array.forEach(callback,[ thisObject])
例子更能說明一切:
var database = { users: ["張含韻", "江一燕", "李小璐"], sendEmail: function (user) { if (this.isValidUser(user)) { console.log("你好," + user); } else { console.log("抱歉,"+ user +",你不是本家人"); } }, isValidUser: function (user) { return /^張/.test(user); } }; // 給每個人法郵件 database.users.forEach( // database.users中人遍歷 database.sendEmail, // 發送郵件 database // 使用database代替上面標紅的this ); // 結果: // 你好,張含韻 // 抱歉,江一燕,你不是本家人 // 抱歉,李小璐,你不是本家
如果這第2個可選參數不指定,則使用全局對象代替(在瀏覽器是為window
),嚴格模式下甚至是undefined
.
另外,forEach不會遍歷純粹“占着官位吃空餉”的元素的,例如下面這個例子:
var array = [1, 2, 3]; delete array[1]; // 移除 2 alert(array); // "1,,3" alert(array.length); // but the length is still 3 array.forEach(alert); // 彈出的僅僅是1和3
綜上全部規則,我們就可以對IE6-IE8進行仿真擴展了,如下代碼:
// 對於古董瀏覽器,如IE6-IE8 if (typeof Array.prototype.forEach != "function") { Array.prototype.forEach = function (fn, context) { for (var k = 0, length = this.length; k < length; k++) { if (typeof fn === "function" && Object.prototype.hasOwnProperty.call(this, k)) { fn.call(context, this[k], k, this); } } }; }
五、ES6新特性
ES6(ECMAScript 6)是即將到來的新版本JavaScript語言的標准。
箭頭操作符
如果你會C#或者Java,你肯定知道lambda表達式,ES6中新增的箭頭操作符=>便有異曲同工之妙。它簡化了函數的書寫。操作符左邊為輸入的參數,而右邊則是進行的操作以及返回的值Inputs=>outputs。
我們知道在JS中回調是經常的事,而一般回調又以匿名函數的形式出現,每次都需要寫一個function,甚是繁瑣。當引入箭頭操作符后可以方便地寫回調了。請看下面的例子。
var array = [1, 2, 3]; //傳統寫法 array.forEach(function(v, i, a) { console.log(v); }); //ES6 array.forEach(v = > console.log(v));
類的支持
ES6中添加了對類的支持,引入了class關鍵字(其實class在JavaScript中一直是保留字,目的就是考慮到可能在以后的新版本中會用到,現在終於派上用場了)。JS本身就是面向對象的,ES6中提供的類實際上只是JS原型模式的包裝。現在提供原生的class支持后,對象的創建,繼承更加直觀了,並且父類方法的調用,實例化,靜態方法和構造函數等概念都更加形象化。
下面代碼展示了類在ES6中的使用。
//類的定義 class Animal { //ES6中新型構造器 constructor(name) { this.name = name; } //實例方法 sayName() { console.log('My name is '+this.name); } } //類的繼承 class Programmer extends Animal { constructor(name) { //直接調用父類構造器進行初始化 super(name); } program() { console.log("I'm coding..."); } } //測試我們的類 var animal=new Animal('dummy'), wayou=new Programmer('wayou'); animal.sayName();//輸出 ‘My name is dummy’ wayou.sayName();//輸出 ‘My name is wayou’ wayou.program();//輸出 ‘I'm coding...’
增強的對象字面量
對象字面量被增強了,寫法更加簡潔與靈活,同時在定義對象的時候能夠做的事情更多了。具體表現在:
- 可以在對象字面量里面定義原型
- 定義方法可以不用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中,利用解構這一特性,可以直接返回一個數組,然后數組中的值會自動被解析到對應接收該值的變量中。
var [x,y]=getVal(),//函數返回值的解構 [name,,age]=['wayou','male','secrect'];//數組解構 function getVal() { return [ 1, 2 ]; } console.log('x:'+x+', y:'+y);//輸出:x:1, y:2 console.log('name:'+name+', age:'+age);//輸出: name:wayou, age:secrect
參數默認值,不定參數,拓展參數
默認參數值
現在可以在定義函數的時候指定參數的默認值了,而不用像以前那樣通過邏輯或操作符來達到目的了。
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
拓展參數
拓展參數則是另一種形式的語法糖,它允許傳遞數組或者類數組直接做為函數的參數而不用通過apply。
var people=['Wayou','John','Sherlock']; //sayHello函數本來接收三個單獨的參數人妖,人二和人三 function sayHello(people1,people2,people3){ console.log(`Hello ${people1},${people2},${people3}`); } //但是我們將一個數組以拓展參數的形式傳遞,它能很好地映射到每個單獨的參數 sayHello(...people);//輸出:Hello Wayou,John,Sherlock //而在以前,如果需要傳遞數組當參數,我們需要使用函數的apply方法 sayHello.apply(null,people);//輸出:Hello Wayou,John,Sherlock
let與const 關鍵字
可以把let看成var,只是它定義的變量被限定在了特定范圍內才能使用,而離開這個范圍則無效。const則很直觀,用來定義常量,即無法被更改值的變量。
for (let i=0;i<2;i++)console.log(i);//輸出: 0,1 console.log(i);//輸出:undefined,嚴格模式下會報錯
for of 值遍歷
我們都知道for in 循環用於遍歷數組,類數組或對象,ES6中新引入的for of循環功能相似,不同的是每次循環它提供的不是序號而是值。
var someArray = [ "a", "b", "c" ]; for (v of someArray) { console.log(v);//輸出 a,b,c }