重讀 ES6 — 字符串、數(shù)值、正則的擴展

從去年開始學(xué)習(xí)及使用 ES6 語法,后得益于項目的推動,到現(xiàn)在已較廣泛的使用 ES6 寫代碼了。

我們知道,ES6 較之 ES5,新增特性非常之多,變化非常大,幾乎像是兩種不同語言。盡管現(xiàn)在 ES6 滿大街了,但仍有許多有用的特性很少觸及。另外,對于已使用的特性,有的使用頻率高,有的使用頻率比較低。

為了在當前基礎(chǔ)上更全面一點的了解 ES6,同時對一些較疑難又非常棒的 API 能更深入的理解,我有個想法就是重新 “畫輪子” 。

雖然個人對這件事有點猶豫,猶豫的點主要因為現(xiàn)在已不乏 ES6 的文檔、資料和問題解答;各路牛人一不小心看到本人整理ES6資料,也許因為理解不到位、或者無用功而扔磚頭;最后,自認為寫東西不擅長,是個耗時的苦差事。

但是,今天還是開始了。

首先,期望的是收獲對 ES6 更全面的了解,其間若能領(lǐng)略一些 ES6語言設(shè)計風(fēng)格,窺視 JavaScript 技術(shù)走向,那也算很好的收獲。另外,新入門的朋友若能因此在 ES6 的學(xué)習(xí)上做一個輕重緩急的劃分,或者收獲些許領(lǐng)悟,也是極好。

好了,重新 “畫輪子”,主要基于以下思路:

  • 強調(diào)從 ES5ES6的差異,進行知識迭代
  • 更注重于 API 的設(shè)計角度來分析新增特性
  • 對技術(shù)點進行邊界管理,將疑難的點先劃在邊界之外,之后再專門突破

縱觀 ES6各種新特性,整理出以下一份清單:

1、基本類型的擴展
2、新增類型和數(shù)據(jù)結(jié)構(gòu)
3、模塊(Module)
4、Promise
5、異步同步化方案 async/await
6、其他偏語言層面的能力開放

大概以 Promise 為分界,分類靠前的更基礎(chǔ),使用頻率更高,相對來說也比較簡單;靠后的是更偏向新增特性,使用頻率稍低,但它們能量可不小。而 Promise 本身,則是一個非常棒的 API。

以下內(nèi)容是上述條目的展開,各有詳略。

基本類型的擴展

很多前端面試,都會問到 JS 的基本類型的理解。

沒錯,在 ES5 的基礎(chǔ)之上, ES6 對這些基本類型進行了一些擴展,擴展并不是平白新增特性,而是將 ES5 的一些高頻使用方式等做了封裝,并 API 化了。

下面依次來看看字符串數(shù)值正則數(shù)組函數(shù)對象以及解構(gòu)賦值這種新的聲明變量的方式。當然,正則不屬于基本類型,這里是借助 “類型的擴展” 這個話題來講的;另外,函數(shù)和對象的擴展是比較重要的部分,會講得稍詳細些。

字符串的擴展

字符串的擴展,主要包括:

  • 新增一些方法
  • 增強對一些其他編碼(特別是 Unicode)的識別能力及計算能力
  • 增加模板字符串。

一、新增的方法

新增的方法 includes(), startsWith(), endsWith() 等基本上是對 indexOf() 方法的封裝。

let str = 'hello world';

//ES6 的 API 直接調(diào)用,語義清晰簡潔
str.endsWith('d');
// => true

//ES5 表達同樣的意思卻要寫一堆
str.indexOf('d') === str.length - 1;

新增的 repeat() 也是一個簡單明了的方法。如果想將一個字符串重復(fù) copy 若干次,在 ES5 是大概率得想到用 for 循環(huán)。

//龐大的循環(huán)語句,重復(fù)字符 `x` 共 n 次
function repeat (n) {
    var str = '';
    for(var i = 0; i < n; i++) {
        str += 'x';
    }
    return str;
}
var str = repeat(10);

//就算靈機一動,使用其他途徑,有點別扭
var str = new Array(10).join('x');

//ES6 推出一個新方法,封裝這一高頻需求
let str = 'x'.repeat(10);

// 當然這個方法的輸入?yún)?shù)有類型等要求,不在此討論范圍

雖然本質(zhì)上都繞不開循環(huán),但是 repeat() 方法明顯更優(yōu)雅。從語言級別來封裝這一使用場景,能從更大規(guī)模上節(jié)省不少的代碼信息量。

二、模板字符串

模板字符串是個非常好的東西,也是我們非常高頻使用的工具。用 backbone.js 那會,就有很多前端同事,以及從其他語言 “入侵” 的同事抱怨過字符串拼接中的一大堆 + 號和惱人的 " '號了。

//惱人的 `+` 號和引號
function welcome (name, place) {
    return 'Hello ' + name + ', welcome to ' + place + '!';
}

//ES6 下 瞬間清爽了
function welcome (name, place) {
    return `Hello ${name}, welcome to ${place}!`;
}

JS 代碼中內(nèi)嵌很多模板片段時,模板字符串無論是處理還是維護,都方便很多。

此外,使用模板字符串可以輕易的實現(xiàn)一個模板庫出來,此處也不再展開。

三、編碼格式(如 Unicode)的相關(guān)擴展

最后,擴展了字符串對其他編碼格式的識別和處理能力,這點目前需求最多(個人感覺)的恐怕的是 i18n 即多語言處理上。這部分的 API 也沒有什么難度,在使用時在把玩一下即可,平時無需增加認知負擔。

數(shù)值的擴展

數(shù)值的擴展,主要的動作是:

  • 將一些 window 下關(guān)于數(shù)值類型的 API 遷移到了更為合理的地方(Number 上)
  • Number 對象上繼續(xù)新增了幾個方法
  • Math 對象上,則擴展了更多的常規(guī)計算函數(shù)
  • 最后還增加了一些對整型、浮點型數(shù)據(jù)溢出的應(yīng)對方法。

一、遷移并提純

數(shù)值的擴展,毫無認知壓力以及讓人容易理解的是上述的第一個動作:將合適的 API 從錯誤的地方(window 對象中)移到正確的地方(Number 對象)下管理。這個如標題所說是 "遷移"。

那“提純”怎么說?——"純函數(shù)",對的。如果還不具備純函數(shù)知識的朋友,若看過本人前一篇文章《走向 JavaScript 函數(shù)式編程》 就知道,遷移后的方法,變成純函數(shù)了——它不再對參數(shù)(的類型)形成 “副作用” 了。

// 有限的數(shù)值,就是有限的
Number.isFinite(9999); // => true

//足夠大、無限數(shù)、非數(shù)值它就不是有限的
Number.isFinite(Math.pow(99, 9999));  // => false
Number.isFinite(Infinity); // => false
Number.isFinite('999');  // => false

// 對于 NaN 判斷 true
Number.isNaN(NaN); // => true
Number.isNaN(999 / NaN); // => true

// 除 NaN 以外一切都判斷 false
Number.isNaN('NaN'); // => false
Number.isNaN(true) // => false

// 這兩個 API 就是為了得到數(shù)值,放在 Number 更合理
Number.parseInt();
Number.parseFloat();

移動后的方法 Number.isFinite()、Number.isNaN() 較之先前也更純粹,它們的參數(shù)不會進行類型自動轉(zhuǎn)化,是字符串就是字符串,是布爾值就是布爾值,不會自作主張轉(zhuǎn)化一下,再去判斷。

而原有的掛載在 window 下的這倆方法的行為讓人捉摸不透。

// 莫名其妙的對參數(shù)進行了類型轉(zhuǎn)化
isFinite('999'); // => true
isNaN('NaN'); // => true

遷移本身的好處是,逐步減少全局性方法,使得語言逐步走向模塊化。本身這點也是 ES6 所倡導(dǎo)的。

二、Number 對象新增計算方法和常量

遷移動作如果是對歷史的修正的話,下邊就是通過新增更多的元素,來豐富 JavaScript 的數(shù)值計算能力。

// 判斷參數(shù)是否是數(shù)值中的整數(shù)
Number.isInteger(28.0); // => true
Number.isSafeInteger(Infinity) // false

// 新增常量,都掛在 Number 對象下
Number.EPSILON === 2.220446049250313e-16; // => true
Number.MAX_SAFE_INTEGER === 9007199254740991; // => true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER; // => true

Number.isInteger(),Number.isSafeInteger() 也是 “提純” 后的方法,即不會擅自轉(zhuǎn)化參數(shù)類型。

上述常量也非常好理解。它們的添加,給實際編程時減少了很多負擔,因為現(xiàn)在任何時候,再也不用自己寫一個 const MAX_INTEGER = Math.pow(2, 53)了,即拿即用就好了。

三、Math 對象新增方法

// 剪掉小數(shù)這個尾巴
Math.trunc(3.141592653);

// 這是新增的一個有用的數(shù)學(xué)函數(shù)
Math.sign(Math.PI/2);  // => 1

// 這是 ES5 就有的三角函數(shù)
Math.sin(Math.PI/2);  // => 1

// 求立方根
Math.cbrt(27); // => 3

// 平方和的平方根 (計算空間距離很方便)
Math.hypot(1, 4, 8); // => 9

//還有很多對數(shù)函數(shù)的方法、雙曲線函數(shù)的方法
//...

一些對數(shù)、雙曲線函數(shù)的方法——除非是一些數(shù)據(jù)處理、科研等項目——在平時并不那么常用,因此不再羅列和展開描述了。

四、數(shù)值的其他擴展

數(shù)值的擴展,最后還增加了一些對整型、浮點型數(shù)據(jù)溢出的應(yīng)對方法。這個需要在平日處理數(shù)據(jù)是特別注意一下。如果不清楚,則在需要的時候查看下文檔,就能輕易掌握。

正則表達式的擴展

正則表達式也許平時編程用得不多,當然也不見得,因項目而異。如果覺得正則實在枯燥的朋友,可以先跳過本知識點,以后按需再學(xué)。

對于剩下的觀眾,我們繼續(xù)。ES6 對正則的一些擴展還是非常有意義的,也是有章可循的。大概和前文關(guān)聯(lián)起來解讀,有這些擴展點:

  • 遷移。像數(shù)值的一些方法那樣,將正則相關(guān)的一些方法歸屬到合理的對象之下;
  • 增加了若干修飾符。尤其是修飾符 u,這和字符串的編碼格式的擴展遙相呼應(yīng);
  • 后行斷言。這是對正則能力完備性的一個修復(fù);
  • 專門擴展了正則匹配時對 Unicode 字符的一些處理。這仍然呼應(yīng)上述第 2 條;
  • 屬名組匹配。這是為語義清晰化做的一個擴展。

本節(jié)只挑 遷移, 后行斷言屬名組匹配 來講,Unicode 再一次被冷落。

一、遷移

一時也找不到專業(yè)的描述,所以類似挪位置的都姑且叫 “遷移”。

字符串的與正則相關(guān)的方法,str.match()str.replace()str.search()str.split() 原本都放在 String.prototype 下實現(xiàn),現(xiàn)在 ES6 有更為合理的歸宿:

String.prototype.match() 
    => RegExp.prototype[Symbol.match]();
    
String.prototype.replace() 
    => RegExp.prototype[Symbol.replace]();
    
String.prototype.search() 
    => RegExp.prototype[Symbol.search]();
    
String.prototype.split() 
    => RegExp.prototype[Symbol.split]();

字符串的上述方法,最終調(diào)用的是 RegExp 上實現(xiàn)的方法。可見, ES6 內(nèi)部還是做了很多優(yōu)化調(diào)整的。

二、后行斷言

ES5 正則表達式已經(jīng)提供這些斷言匹配:(?:pattern)(?=pattern)(?!pattern)。僅拿第二個來舉例:

// (?=26|27|28) 好比正則中的正則表達式
let reg = /Today(?=26|27|28)/g; 
// => 它能匹配到 'Today27' 中的 'Today'
// => 但無法匹配到 'Today29' 中的 'Today'
// => (?=pattern) 表達式只起到輔助驗證作用,如果驗證通過,則其輔佐的對象將成為最終的結(jié)果

可以看到,這種斷言是先有主體,再有輔助表達式,意味著——如果輔助表達式匹配成功——前半部分就是本次全部匹配能夠獲取的目標。

那問題來了,我想匹配到后面的目標,而讓前半部分成為輔佐匹配,那豈不是難以辦到?確實,筆者曾經(jīng)就遇到過這個麻煩。

現(xiàn)在 ES6 正好彌補了這個缺陷,提供了 “后行斷言” 的規(guī)則,只要在 “先行斷言” 的問號后加上 '<' 就是了。它們分別是:(?<:pattern)(?<=pattern)(?<!pattern)

let reg = /(?<=26|27|28)Today/g; 
// => 它能匹配到 '27Today' 中的 'Today'

三、屬名組匹配

ES5 其實也有屬名組,就是匹配到一組 ( ) 中的值時,可以用特定的字符 $1/$2/$3 來表示。

這個看起來像模板字符串的東西,大家想必都見過:

// $1/$2/$3 依次捕獲正則中的 '( )' 括號匹配到的值
let reg = /(\d{4})-(\d{2})-(\d{2})/;
'2017-8-27'.replace(reg, '$1/$2/$3');
// => '2017/8/27'

但是這樣仍然非常不直觀,幾乎沒有語義化可言。ES6 的做法是,允許在匹配模式前加一個簽名比如 <year>,表明將來匹配到的這個內(nèi)容,直接賦值給了 year 變量。

let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
let matched = reg.exec('2017-8-27');
let year = matched.groups.year; // => 2017
let month = matched.groups.month; // => 8
let day = matched.groups.day; // => 27

正則還有很多擴展沒有講到,但以上應(yīng)該是使用頻率比較高的幾個部分了。另外,ES6 對正則的擴展已深入到比較細致的部分,也就是賦予了正則表達式更多的編程樂趣。

總結(jié)

以上對 JavaScript2015 基本類型中的字符串數(shù)值以及 正則的擴展部分進行了梳理,該部分比較簡單,也非常瑣碎。但也能充分凸顯 ES6 相比 ES5 細節(jié)處變化之大。

下一篇《基本類型的擴展(二)》會講到數(shù)組、函數(shù)、對象的擴展。敬請期待。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容