1. 程式人生 > >ES6學習筆記:字串、正則、數值、函式和陣列的擴充套件

ES6學習筆記:字串、正則、數值、函式和陣列的擴充套件

字串的擴充套件

includes():返回布林值,表示是否找到了引數字串。
startsWith():返回布林值,表示引數字串是否在原字串的頭部。
endsWith():返回布林值,表示引數字串是否在原字串的尾部。

let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

這三個方法都支援第二個引數,表示開始搜尋的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith
('Hello', 5) // true s.includes('Hello', 6) // false

上面程式碼表示,使用第二個引數n時,endsWith的行為與其他兩個方法有所不同。它針對前n個字元,而其他兩個方法針對從第n個位置直到字串結束。

repeat()方法返回一個新字串,表示將原字串重複n次。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

引數如果是小數,會被取整。

'na'.repeat(2.9) // "nana"

如果repeat的引數是負數或者Infinity,會報錯。

'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError

但是,如果引數是0到-1之間的小數,則等同於0,這是因為會先進行取整運算。0到-1之間的小數,取整以後等於-0,repeat視同為0。

'na'.repeat(-0.9) // ""

引數NaN等同於0。

'na'.repeat(NaN) // ""

如果repeat的引數是字串,則會先轉換成數字。

'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"

ES2017 引入了字串補全長度的功能。如果某個字串不夠指定長度,會在頭部或尾部補全。padStart()用於頭部補全,padEnd()用於尾部補全。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

上面程式碼中,padStart和padEnd一共接受兩個引數,第一個引數用來指定字串的最小長度,第二個引數是用來補全的字串。

如果原字串的長度,等於或大於指定的最小長度,則返回原字串。

'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'

如果用來補全的字串與原字串,兩者的長度之和超過了指定的最小長度,則會截去超出位數的補全字串。

'abc'.padStart(10, '0123456789')
// '0123456abc'

如果省略第二個引數,預設使用空格補全長度。

'x'.padStart(4) // '   x'
'x'.padEnd(4) // 'x   '

padStart的常見用途是為數值補全指定位數。下面程式碼生成10位的數值字串。

'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"

另一個用途是提示字串格式。

'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

模板字串(template string)是增強版的字串,用反引號(`)標識。它可以當作普通字串使用,也可以用來定義多行字串,或者在字串中嵌入變數。

// 普通字串
`In JavaScript '\n' is a line-feed.`

// 多行字串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字串中嵌入變數
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

上面程式碼中的模板字串,都是用反引號表示。如果在模板字串中需要使用反引號,則前面要用反斜槓轉義。

let greeting = `\`Yo\` World!`;

如果使用模板字串表示多行字串,所有的空格和縮排都會被保留在輸出之中。

正則的擴充套件

ES6 為正則表示式添加了y修飾符,叫做“粘連”(sticky)修飾符。

y修飾符的作用與g修飾符類似,也是全域性匹配,後一次匹配都從上一次匹配成功的下一個位置開始。不同之處在於,g修飾符只要剩餘位置中存在匹配就可,而y修飾符確保匹配必須從剩餘的第一個位置開始,這也就是“粘連”的涵義。

var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;

r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]

r1.exec(s) // ["aa"]
r2.exec(s) // null

與y修飾符相匹配,ES6 的正則物件多了sticky屬性,表示是否設定了y修飾符。
ES6 為正則表示式新增了flags屬性,會返回正則表示式的修飾符。

// ES5 的 source 屬性
// 返回正則表示式的正文
/abc/ig.source
// "abc"

// ES6 的 flags 屬性
// 返回正則表示式的修飾符
/abc/ig.flags
// 'gi'

”先行斷言“指的是,x只有在y前面才匹配,必須寫成/x(?=y)/。比如,只匹配百分號之前的數字,要寫成/\d+(?=%)/。”先行否定斷言“指的是,x只有不在y前面才匹配,必須寫成/x(?!y)/。比如,只匹配不在百分號之前的數字,要寫成/\d+(?!%)/。

/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them')                 // ["44"]

上面兩個字串,如果互換正則表示式,就不會得到相同結果。另外,還可以看到,”先行斷言“括號之中的部分((?=%)),是不計入返回結果的。

“後行斷言”正好與“先行斷言”相反,x只有在y後面才匹配,必須寫成/(?<=y)x/。比如,只匹配美元符號之後的數字,要寫成/(?<=$)\d+/。”後行否定斷言“則與”先行否定斷言“相反,x只有不在y後面才匹配,必須寫成/(?

/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90"]

上面的例子中,“後行斷言”的括號之中的部分((?<=$)),也是不計入返回結果。
下面的例子是使用後行斷言進行字串替換。

const RE_DOLLAR_PREFIX = /(?<=$)foo/g;
foo//bar %foo foo’
上面程式碼中,只有在美元符號後面的foo才會被替換。

“後行斷言”的實現,需要先匹配/(?<=y)x/的x,然後再回到左邊,匹配y的部分。這種“先右後左”的執行順序,與所有其他正則操作相反,導致了一些不符合預期的行為。

首先,”後行斷言“的組匹配,與正常情況下結果是不一樣的。

/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]

上面程式碼中,需要捕捉兩個組匹配。沒有”後行斷言”時,第一個括號是貪婪模式,第二個括號只能捕獲一個字元,所以結果是105和3。而”後行斷言”時,由於執行順序是從右到左,第二個括號是貪婪模式,第一個括號只能捕獲一個字元,所以結果是1和053。

其次,”後行斷言”的反斜槓引用,也與通常的順序相反,必須放在對應的那個括號之前。

/(?<=(o)d\1)r/.exec('hodor')  // null
/(?<=\1d(o))r/.exec('hodor')  // ["r", "o"]

上面程式碼中,如果後行斷言的反斜槓引用(\1)放在括號的後面,就不會得到匹配結果,必須放在前面才可以。因為後行斷言是先從左到右掃描,發現匹配以後再回過頭,從右到左完成反斜槓引用。

現在有一個“具名組匹配”(Named Capture Groups)的提案,允許為每一個組匹配指定一個名字,既便於閱讀程式碼,又便於引用。

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

上面程式碼中,“具名組匹配”在圓括號內部,模式的頭部新增“問號 + 尖括號 + 組名”(?),然後就可以在exec方法返回結果的groups屬性上引用該組名。同時,數字序號(matchObj[1])依然有效。

具名組匹配等於為每一組匹配加上了 ID,便於描述匹配的目的。如果組的順序變了,也不用改變匹配後的處理程式碼。

如果具名組沒有匹配,那麼對應的groups物件屬性會是undefined。

如果要在正則表示式內部引用某個“具名組匹配”,可以使用\k<組名>的寫法。

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false

數值擴充套件

ES6 在Number物件上,新提供了Number.isFinite()和Number.isNaN()兩個方法。

Number.isFinite()用來檢查一個數值是否為有限的(finite)。

Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false

Number.isNaN()用來檢查一個值是否為NaN。

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true'/0) // true
Number.isNaN('true'/'true') // true

它們與傳統的全域性方法isFinite()和isNaN()的區別在於,傳統方法先呼叫Number()將非數值的值轉為數值,再進行判斷,而這兩個新方法只對數值有效,Number.isFinite()對於非數值一律返回false, Number.isNaN()只有對於NaN才返回true,非NaN一律返回false。

isFinite(25) // true
isFinite("25") // true
Number.isFinite(25) // true
Number.isFinite("25") // false

isNaN(NaN) // true
isNaN("NaN") // true
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false
Number.isNaN(1) // false

Number.isInteger()用來判斷一個值是否為整數。需要注意的是,在 JavaScript 內部,整數和浮點數是同樣的儲存方法,所以3和3.0被視為同一個值。

Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false

ES6 在Number物件上面,新增一個極小的常量Number.EPSILON。根據規格,它表示1與大於1的最小浮點數之間的差(2的-52次方)。引入一個這麼小的量的目的,在於為浮點數計算,設定一個誤差範圍。我們知道浮點數計算是不精確的。

0.1 + 0.2
// 0.30000000000000004

JavaScript 能夠準確表示的整數範圍在-2^53到2^53之間(不含兩個端點),ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER這兩個常量,用來表示這個範圍的上下限。

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true

Number.isSafeInteger()則是用來判斷一個整數是否落在這個範圍之內。

Math.trunc方法用於去除一個數的小數部分,返回整數部分。

Math.sign方法用來判斷一個數到底是正數(+1)、負數(-1)、還是零(0,-0)或者NaN。對於非數值,會先將其轉換為數值。

Math.cbrt方法用於計算一個數的立方根。

函式的擴充套件

ES6 允許為函式的引數設定預設值,即直接寫在引數定義的後面。

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World

引數變數是預設宣告的,所以不能用let或const再次宣告。

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

使用引數預設值時,函式不能有同名引數。

// 不報錯
function foo(x, x, y) {
  // ...
}

// 報錯
function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

引數預設值不是傳值的,而是每次都重新計算預設值表示式的值。也就是說,引數預設值是惰性求值的。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

引數預設值可以與解構賦值的預設值,結合起來使用。

function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined

function foo({x, y = 5} = {}) {
  console.log(x, y);
}

foo() // undefined 5

通常情況下,定義了預設值的引數,應該是函式的尾引數。如果非尾部的引數設定預設值,實際上這個引數是沒法省略的。

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]

上面程式碼中,有預設值的引數都不是尾引數。這時,無法只省略該引數,而不省略它後面的引數,除非顯式輸入undefined。

指定了預設值以後,函式的length屬性,將返回沒有指定預設值的引數個數。也就是說,指定了預設值後,length屬性將失真。這是因為length屬性的含義是,該函式預期傳入的引數個數。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

同理,後文的 rest 引數也不會計入length屬性。

(function(...args) {}).length // 0

如果設定了預設值的引數不是尾引數,那麼length屬性也不再計入後面的引數了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

一旦設定了引數的預設值,函式進行宣告初始化時,引數會形成一個單獨的作用域(context)。等到初始化結束,這個作用域就會消失。這種語法行為,在不設定引數預設值時,是不會出現的。

var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2

上面程式碼中,引數y的預設值等於變數x。呼叫函式f時,引數形成一個單獨的作用域。在這個作用域裡面,預設值變數x指向第一個引數x,而不是全域性變數x,所以輸出是2。

再看下面的例子。

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

上面程式碼中,函式f呼叫時,引數y = x形成一個單獨的作用域。這個作用域裡面,變數x本身沒有定義,所以指向外層的全域性變數x。函式呼叫時,函式體內部的區域性變數x影響不到預設值變數x。

如果引數的預設值是一個函式,該函式的作用域也遵守這個規則。請看下面的例子。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1

上面程式碼中,函式foo的引數形成一個單獨作用域。這個作用域裡面,首先聲明瞭變數x,然後聲明瞭變數y,y的預設值是一個匿名函式。這個匿名函式內部的變數x,指向同一個作用域的第一個引數x。函式foo內部又聲明瞭一個內部變數x,該變數與第一個引數x由於不是同一個作用域,所以不是同一個變數,因此執行y後,內部變數x和外部全域性變數x的值都沒變。

如果將var x = 3的var去除,函式foo的內部變數x就指向第一個引數x,與匿名函式內部的x是一致的,所以最後輸出的就是2,而外層的全域性變數x依然不受影響。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}

foo() // 2
x // 1

ES6 引入 rest 引數(形式為…變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。rest 引數搭配的變數是一個數組,該變數將多餘的引數放入陣列中。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

rest 引數之後不能再有其他引數(即只能是最後一個引數),否則會報錯。函式的length屬性,不包括 rest 引數。

函式的name屬性,返回該函式的函式名。

function foo() {}
foo.name // "foo"

這個屬性早就被瀏覽器廣泛支援,但是直到 ES6,才將其寫入了標準。需要注意的是,ES6 對這個屬性的行為做出了一些修改。如果將一個匿名函式賦值給一個變數,ES5 的name屬性,會返回空字串,而 ES6 的name屬性會返回實際的函式名。

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

陣列的擴充套件

擴充套件運算子(spread)是三個點(…)。它好比 rest 引數的逆運算,將一個數組轉為用逗號分隔的引數序列。

console.log(...[1, 2, 3])
// 1 2 3

擴充套件運算子提供了複製陣列的簡便寫法。

const a1 = [1, 2];
// 寫法一
const a2 = [...a1];
// 寫法二
const [...a2] = a1;

上面的兩種寫法,a2都是a1的克隆。

擴充套件運算子提供了數組合並的新寫法。

// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];

// ES5的合併陣列
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6的合併陣列
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

如果將擴充套件運算子用於陣列賦值,只能放在引數的最後一位,否則會報錯。

const [...butLast, last] = [1, 2, 3, 4, 5];
// 報錯

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 報錯

擴充套件運算子還可以將字串轉為真正的陣列。

[...'hello']
// [ "h", "e", "l", "l", "o" ]

上面的寫法,有一個重要的好處,那就是能夠正確識別四個位元組的 Unicode 字元。

'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3

任何 Iterator 介面的物件(參閱 Iterator 一章),都可以用擴充套件運算子轉為真正的陣列。

let nodeList = document.querySelectorAll('div');
let array = [...nodeList];

上面程式碼中,querySelectorAll方法返回的是一個nodeList物件。它不是陣列,而是一個類似陣列的物件。這時,擴充套件運算子可以將其轉為真正的陣列,原因就在於NodeList物件實現了 Iterator 。

擴充套件運算子內部呼叫的是資料結構的 Iterator 介面,因此只要具有 Iterator 介面的物件,都可以使用擴充套件運算子,比如 Map 結構和遍歷器物件。

Array.from方法用於將兩類物件轉為真正的陣列:類似陣列的物件(array-like object)和可遍歷(iterable)的物件(包括ES6新增的資料結構Set和Map)。下面是一個類似陣列的物件,Array.from將它轉為真正的陣列。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的寫法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的寫法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

實際應用中,常見的類似陣列的物件是DOM操作返回的NodeList集合,以及函式內部的arguments物件。Array.from都可以將它們轉為真正的陣列。Array.from方法還支援類似陣列的物件。所謂類似陣列的物件,本質特徵只有一點,即必須有length屬性。因此,任何有length屬性的物件,都可以通過Array.from方法轉為陣列,而此時擴充套件運算子就無法轉換。Array.from還可以接受第二個引數,作用類似於陣列的map方法,用來對每個元素進行處理,將處理後的值放入返回的陣列。

Array.from(arrayLike, x => x * x);
// 等同於
Array.from(arrayLike).map(x => x * x);

如果map函式裡面用到了this關鍵字,還可以傳入Array.from的第三個引數,用來繫結this。

Array.of方法用於將一組值,轉換為陣列。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

這個方法的主要目的,是彌補陣列建構函式Array()的不足。因為引數個數的不同,會導致Array()的行為有差異。

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

上面程式碼中,Array方法沒有引數、一個引數、三個引數時,返回結果都不一樣。只有當引數個數不少於2個時,Array()才會返回由引數組成的新陣列。引數個數只有一個時,實際上是指定陣列的長度。Array.of基本上可以用來替代Array()或new Array().

陣列例項的copyWithin方法,在當前陣列內部,將指定位置的成員複製到其他位置(會覆蓋原有成員),然後返回當前陣列。也就是說,使用這個方法,會修改當前陣列。

Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三個引數。

target(必需):從該位置開始替換資料。
start(可選):從該位置開始讀取資料,預設為0。如果為負值,表示倒數。
end(可選):到該位置前停止讀取資料,預設等於陣列長度。如果為負值,表示倒數。
這三個引數都應該是數值,如果不是,會自動轉為數值。

[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

陣列例項的find方法,用於找出第一個符合條件的陣列成員。它的引數是一個回撥函式,所有陣列成員依次執行該回調函式,直到找出第一個返回值為true的成員,然後返回該成員。如果沒有符合條件的成員,則返回undefined。

[1, 4, -5, 10].find((n) => n < 0)
// -5

陣列例項的findIndex方法的用法與find方法非常類似,返回第一個符合條件的陣列成員的位置,如果所有成員都不符合條件,則返回-1。

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

這兩個方法都可以接受第二個引數,用來繫結回撥函式的this物件。

fill方法使用給定值,填充一個數組。

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

上面程式碼表明,fill方法用於空陣列的初始化非常方便。陣列中已有的元素,會被全部抹去。

fill方法還可以接受第二個和第三個引數,用於指定填充的起始位置和結束位置。

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

ES6 提供三個新的方法——entries(),keys()和values()——用於遍歷陣列。它們都返回一個遍歷器物件(詳見《Iterator》一章),可以用for…of迴圈進行遍歷,唯一的區別是keys()是對鍵名的遍歷、values()是對鍵值的遍歷,entries()是對鍵值對的遍歷。

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

如果不使用for…of迴圈,可以手動呼叫遍歷器物件的next方法,進行遍歷。

let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']