JavaScript 優(yōu)雅的實(shí)現(xiàn)方式包含你可能不知道的知識(shí)點(diǎn)

有些東西很好用架馋,但是你未必知道;有些東西你可能用過全闷,但是你未必知道原理叉寂。

實(shí)現(xiàn)一個(gè)目的有多種途徑,俗話說总珠,條條大路通羅馬屏鳍。很多內(nèi)容來自平時(shí)的一些收集以及過往博客文章底下的精彩評(píng)論,收集整理拓展一波局服,發(fā)散一下大家的思維以及拓展一下知識(shí)面钓瞭。

茴字有四種寫法,233333...淫奔, 文末有彩蛋有驚喜山涡。

1、簡短優(yōu)雅地實(shí)現(xiàn) sleep 函數(shù)

很多語言都有 sleep 函數(shù),顯然 js 沒有鸭丛,那么如何能簡短優(yōu)雅地實(shí)現(xiàn)這個(gè)方法竞穷?

1.1 普通版

function sleep(sleepTime) {
    for(var start = +new Date; +new Date - start <= sleepTime;) {}
}
var t1 = +new Date()
sleep(3000)
var t2 = +new Date()
console.log(t2 - t1)

優(yōu)點(diǎn):簡單粗暴,通俗易懂鳞溉。
缺點(diǎn):這是最簡單粗暴的實(shí)現(xiàn)瘾带,確實(shí) sleep 了,也確實(shí)卡死了穿挨,CPU 會(huì)飆升月弛,無論你的服務(wù)器 CPU 有多么 Niubility。

1.2 Promise 版本

function sleep(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

const t1 = +new Date()
sleep(3000).then(() => {
  const t2 = +new Date()
  console.log(t2 - t1)
})

優(yōu)點(diǎn):這種方式實(shí)際上是用了 setTimeout科盛,沒有形成進(jìn)程阻塞,不會(huì)造成性能和負(fù)載問題菜皂。
缺點(diǎn):雖然不像 callback 套那么多層贞绵,但仍不怎么美觀,而且當(dāng)我們需要在某過程中需要停止執(zhí)行(或者在中途返回了錯(cuò)誤的值)恍飘,還必須得層層判斷后跳出榨崩,非常麻煩,而且這種異步并不是那么徹底章母,還是看起來別扭母蛛。

1.3 Generator 版本

function sleep(delay) {
  return function(cb) {
    setTimeout(cb.bind(this), delay)
  };
}

function* genSleep() {
  const t1 = +new Date()
  yield sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
}

async(genSleep)

function async(gen) {
  const iter = gen()
  function nextStep(it) {
    if (it.done) return
    if (typeof it.value === "function") {
      it.value(function(ret) {
        nextStep(iter.next(ret))
      })
    } else {
      nextStep(iter.next(it.value))
    }
  }
  nextStep(iter.next())
}

優(yōu)點(diǎn):同 Promise 優(yōu)點(diǎn),另外代碼就變得非常簡單干凈乳怎,沒有 then 那么生硬和惡心彩郊。
缺點(diǎn):但不足也很明顯,就是每次都要執(zhí)行 next() 顯得很麻煩蚪缀,雖然有 co(第三方包)可以解決秫逝,但就多包了一層,不好看询枚,錯(cuò)誤也必須按 co 的邏輯來處理立磁,不爽艾恼。

co 之所以這么火并不是沒有原因的,當(dāng)然不是僅僅實(shí)現(xiàn) sleep 這么無聊的事情,而是它活生生的借著generator/yield 實(shí)現(xiàn)了很類似 async/await 的效果沿彭!這一點(diǎn)真是讓我三觀盡毀刮目相看。

const co = require("co")
function sleep(delay) {
  return function(cb) {
    setTimeout(cb.bind(this), delay)
  }
}

co(function*() {
  const t1 = +new Date()
  yield sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
})

1.4 Async/Await 版本

function sleep(delay) {
  return new Promise(reslove => {
    setTimeout(reslove, delay)
  })
}

!async function test() {
  const t1 = +new Date()
  await sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
}()

優(yōu)點(diǎn):同 Promise 和 Generator 優(yōu)點(diǎn)节仿。 Async/Await 可以看做是 Generator 的語法糖巢块,Async 和 Await 相較于 * 和 yield 更加語義,另外各個(gè)函數(shù)都是扁平的抒线,不會(huì)產(chǎn)生多余的嵌套班巩,代碼更加清爽易讀。
缺點(diǎn): ES7 語法存在兼容性問題,有 babel 一切兼容性都不是問題

至于 Async/AwaitPromiseGenerator 的好處可以參考這兩篇文章:
Async/Await 比 Generator 的四個(gè)改進(jìn)點(diǎn)
關(guān)于Async/Await替代Promise的6個(gè)理由

1.5 不要忘了開源的力量

在 javascript 優(yōu)雅的寫 sleep 等于如何優(yōu)雅的不優(yōu)雅抱慌,2333

這里有 C++ 實(shí)現(xiàn)的模塊:https://github.com/ErikDubbelboer/node-sleep

const sleep = require("sleep")

const t1 = +new Date()
sleep.msleep(3000)
const t2 = +new Date()
console.log(t2 - t1)

優(yōu)點(diǎn):能夠?qū)崿F(xiàn)更加精細(xì)的時(shí)間精確度逊桦,而且看起來就是真的 sleep 函數(shù),清晰直白抑进。
缺點(diǎn):缺點(diǎn)需要安裝這個(gè)模塊强经,^_^,這也許算不上什么缺點(diǎn)寺渗。

從一個(gè)間簡簡單單的 sleep 函數(shù)我們就就可以管中窺豹匿情,看見 JavaScript 近幾年來不斷快速的發(fā)展,不單單是異步編程這一塊信殊,還有各種各樣的新技術(shù)和新框架炬称,見證了 JavaScript 的繁榮。

你可能不知道的前端知識(shí)點(diǎn):Async/Await是目前前端異步書寫最優(yōu)雅的一種方式

2涡拘、獲取時(shí)間戳

上面第一個(gè)用多種方式實(shí)現(xiàn) sleep 函數(shù)玲躯,我們可以發(fā)現(xiàn)代碼有 +new Date()獲取時(shí)間戳的用法,這只是其中的一種鳄乏,下面就說一下其他兩種以及 +new Date()的原理跷车。

2.1 普通版

var timestamp=new Date().getTime()

優(yōu)點(diǎn):具有普遍性,大家都用這個(gè)
缺點(diǎn):目前沒有發(fā)現(xiàn)

2.2 進(jìn)階版

var timestamp = (new Date()).valueOf()

valueOf 方法返回對(duì)象的原始值(Primitive,'Null','Undefined','String','Boolean','Number'五種基本數(shù)據(jù)類型之一)橱野,可能是字符串朽缴、數(shù)值或 bool 值等,看具體的對(duì)象水援。

優(yōu)點(diǎn):說明開發(fā)者原始值有一個(gè)具體的認(rèn)知密强,讓人眼前一亮。
缺點(diǎn): 目前沒有發(fā)現(xiàn)

2.3 終極版

var timestamp = +new Date()

優(yōu)點(diǎn):對(duì) JavaScript 隱式轉(zhuǎn)換掌握的比較牢固的一個(gè)表現(xiàn)
缺點(diǎn):目前沒有發(fā)現(xiàn)

現(xiàn)在就簡單分析一下為什么 +new Date() 拿到的是時(shí)間戳裹唆。

一言以蔽之誓斥,這是隱式轉(zhuǎn)換的玄學(xué),實(shí)質(zhì)還是調(diào)用了 valueOf() 的方法许帐。

我們先看看 ECMAScript 規(guī)范對(duì)一元運(yùn)算符的規(guī)范:

一元+ 運(yùn)算符

一元 + 運(yùn)算符將其操作數(shù)轉(zhuǎn)換為 Number 類型并反轉(zhuǎn)其正負(fù)劳坑。注意負(fù)的 +0 產(chǎn)生 -0,負(fù)的 -0 產(chǎn)生 +0成畦。產(chǎn)生式 UnaryExpression : - UnaryExpression 按照下面的過程執(zhí)行距芬。

  1. 令 expr 為解釋執(zhí)行 UnaryExpression 的結(jié)果 .
  2. 令 oldValue 為 ToNumber(GetValue(expr)).
  3. 如果 oldValue is NaN ,return NaN.
  4. 返回 oldValue 取負(fù)(即循帐,算出一個(gè)數(shù)字相同但是符號(hào)相反的值)的結(jié)果框仔。

+new Date() 相當(dāng)于 ToNumber(new Date())

我們?cè)賮砜纯?ECMAScript 規(guī)范對(duì) ToNumber 的定義:

我們知道 new Date() 是個(gè)對(duì)象,滿足上面的 ToPrimitive()拄养,所以進(jìn)而成了 ToPrimitive(new Date())离斩。

接著我們?cè)賮砜纯?ECMAScript 規(guī)范對(duì) ToPrimitive 的定義银舱,一層一層來,抽絲剝繭跛梗。

這個(gè) ToPrimitive 剛開始可能不太好懂寻馏,我來大致解釋一下吧:

ToPrimitive(obj,preferredType)

JavaScript 引擎內(nèi)部轉(zhuǎn)換為原始值 ToPrimitive(obj,preferredType) 函數(shù)接受兩個(gè)參數(shù),第一個(gè) obj 為被轉(zhuǎn)換的對(duì)象核偿,第二個(gè)preferredType 為希望轉(zhuǎn)換成的類型(默認(rèn)為空诚欠,接受的值為 NumberString

在執(zhí)行 ToPrimitive(obj,preferredType) 時(shí)如果第二個(gè)參數(shù)為空并且 objDate 的實(shí)例時(shí),此時(shí) preferredType 會(huì)被設(shè)置為 String漾岳,其他情況下 preferredType 都會(huì)被設(shè)置為Number 如果 preferredTypeNumber轰绵,ToPrimitive 執(zhí)行過程如下:

  1. 如果obj為原始值,直接返回尼荆;
  2. 否則調(diào)用 obj.valueOf()左腔,如果執(zhí)行結(jié)果是原始值,返回之耀找;
  3. 否則調(diào)用 obj.toString()翔悠,如果執(zhí)行結(jié)果是原始值,返回之野芒;
  4. 否則拋異常。

如果 preferredTypeString双炕,將上面的第2步和第3步調(diào)換狞悲,即:

  1. 如果obj為原始值,直接返回妇斤;
  2. 否則調(diào)用 obj.toString()摇锋,如果執(zhí)行結(jié)果是原始值,返回之站超;
  3. 否則調(diào)用 obj.valueOf()荸恕,如果執(zhí)行結(jié)果是原始值,返回之死相;
  4. 否則拋異常融求。

首先我們要明白 obj.valueOf()obj.toString() 還有原始值分別是什么意思,這是弄懂上面描述的前提之一:

toString 用來返回對(duì)象的字符串表示。

var obj = {};
console.log(obj.toString());//[object Object]

var arr2 = [];
console.log(arr2.toString());//""空字符串

var date = new Date();
console.log(date.toString());//Sun Feb 28 2016 13:40:36 GMT+0800 (中國標(biāo)準(zhǔn)時(shí)間)

valueOf 方法返回對(duì)象的原始值算撮,可能是字符串生宛、數(shù)值或 bool 值等,看具體的對(duì)象肮柜。

var obj = {
  name: "obj"
}
console.log(obj.valueOf()) //Object {name: "obj"}

var arr1 = [1]
console.log(arr1.valueOf()) //[1]

var date = new Date()
console.log(date.valueOf())//1456638436303
如代碼所示陷舅,三個(gè)不同的對(duì)象實(shí)例調(diào)用valueOf返回不同的數(shù)據(jù)

原始值指的是 'Null','Undefined','String','Boolean','Number','Symbol' 6種基本數(shù)據(jù)類型之一,上面已經(jīng)提到過這個(gè)概念审洞,這里再次申明一下莱睁。

最后分解一下其中的過程:+new Date():

  1. 運(yùn)算符 new 的優(yōu)先級(jí)高于一元運(yùn)算符 +,所以過程可以分解為:
    var time=new Date();
    +time
  2. 根據(jù)上面提到的規(guī)則相當(dāng)于:ToNumber(time)
  3. time 是個(gè)日期對(duì)象,根據(jù) ToNumber 的轉(zhuǎn)換規(guī)則仰剿,所以相當(dāng)于:ToNumber(ToPrimitive(time))
  4. 根據(jù) ToPrimitive 的轉(zhuǎn)換規(guī)則:ToNumber(time.valueOf())time.valueOf() 就是 原始值 得到的是個(gè)時(shí)間戳酥馍,假設(shè) time.valueOf()=1503479124652
  5. 所以 ToNumber(1503479124652) 返回值是 1503479124652 這個(gè)數(shù)字辩昆。
  6. 分析完畢

你可能不知道的前端知識(shí)點(diǎn):隱式轉(zhuǎn)換的妙用

3、數(shù)組去重

注:暫不考慮對(duì)象字面量旨袒,函數(shù)等引用類型的去重汁针,也不考慮 NaN, undefined, null等特殊類型情況。

數(shù)組樣本:[1, 1, '1', '2', 1]

3.1 普通版

無需思考砚尽,我們可以得到 O(n^2) 復(fù)雜度的解法施无。定義一個(gè)變量數(shù)組 res 保存結(jié)果,遍歷需要去重的數(shù)組必孤,如果該元素已經(jīng)存在在 res 中了猾骡,則說明是重復(fù)的元素,如果沒有敷搪,則放入 res 中兴想。

var a = [1, 1, '1', '2', 1]
function unique(arr) {
    var res = []
    for (var i = 0, len = arr.length; i < len; i++) {
        var item = arr[i]
        for (var j = 0, len = res.length; j < jlen; j++) {
            if (item === res[j]) //arr數(shù)組的item在res已經(jīng)存在,就跳出循環(huán)
                break
        }
        if (j === jlen) //循環(huán)完畢,arr數(shù)組的item在res找不到,就push到res數(shù)組中
            res.push(item)
    }
    return res
}
console.log(unique(a)) // [1, 2, "1"]

優(yōu)點(diǎn): 沒有任何兼容性問題,通俗易懂赡勘,沒有任何理解成本
缺點(diǎn): 看起來比較臃腫比較繁瑣嫂便,時(shí)間復(fù)雜度比較高O(n^2)

3.2 進(jìn)階版

var a =  [1, 1, '1', '2', 1]
function unique(arr) {
    return arr.filter(function(ele,index,array){
        return array.indexOf(ele) === index//很巧妙,這樣篩選一對(duì)一的,過濾掉重復(fù)的
    })
}
console.log(unique(a)) // [1, 2, "1"]

優(yōu)點(diǎn):很簡潔,思維也比較巧妙闸与,直觀易懂毙替。
缺點(diǎn):不支持 IE9 以下的瀏覽器,時(shí)間復(fù)雜度還是O(n^2)

3.3 時(shí)間復(fù)雜度為O(n)

var a =  [1, 1, '1', '2', 1]
function unique(arr) {
    var obj = {}
    return arr.filter(function(item, index, array){
        return obj.hasOwnProperty(typeof item + item) ? 
        false : 
        (obj[typeof item + item] = true)
    })
}

console.log(unique(a)) // [1, 2, "1"]

優(yōu)點(diǎn):hasOwnProperty 是對(duì)象的屬性(名稱)存在性檢查方法践樱。對(duì)象的屬性可以基于 Hash 表實(shí)現(xiàn)厂画,因此對(duì)屬性進(jìn)行訪問的時(shí)間復(fù)雜度可以達(dá)到O(1);
filter 是數(shù)組迭代的方法,內(nèi)部還是一個(gè) for 循環(huán)拷邢,所以時(shí)間復(fù)雜度是 O(n)袱院。
缺點(diǎn):不兼容 IE9 以下瀏覽器,其實(shí)也好解決解孙,把 filter 方法用 for 循環(huán)代替或者自己模擬一個(gè) filter 方法坑填。

3.4 終極版

以 Set 為例,ES6 提供了新的數(shù)據(jù)結(jié)構(gòu) Set弛姜。它類似于數(shù)組脐瑰,但是成員的值都是唯一的,沒有重復(fù)的值廷臼。

const unique = a => [...new Set(a)]

優(yōu)點(diǎn):ES6 語法苍在,簡潔高效绝页,我們可以看到,去重方法從原始的 14 行代碼到 ES61 行代碼寂恬,其實(shí)也說明了 JavaScript 這門語言在不停的進(jìn)步续誉,相信以后的開發(fā)也會(huì)越來越高效。
缺點(diǎn):兼容性問題初肉,現(xiàn)代瀏覽器才支持酷鸦,有 babel 這些都不是問題。

你可能不知道的前端知識(shí)點(diǎn):ES6 新的數(shù)據(jù)結(jié)構(gòu) Set 去重

4牙咏、數(shù)字格式化 1234567890 --> 1,234,567,890

4.1 普通版

function formatNumber(str) {
  let arr = [],
    count = str.length

  while (count >= 3) {
    arr.unshift(str.slice(count - 3, count))
    count -= 3
  }

  // 如果是不是3的倍數(shù)就另外追加到上去
  str.length % 3 && arr.unshift(str.slice(0, str.length % 3))

  return arr.toString()

}
console.log(formatNumber("1234567890")) // 1,234,567,890

優(yōu)點(diǎn):自我感覺比網(wǎng)上寫的一堆 for循環(huán) 還有 if-else 判斷的邏輯更加清晰直白臼隔。
缺點(diǎn):太普通

4.2 進(jìn)階版

function formatNumber(str) {

  // ["0", "9", "8", "7", "6", "5", "4", "3", "2", "1"]
  return str.split("").reverse().reduce((prev, next, index) => {
    return ((index % 3) ? next : (next + ',')) + prev
  })
}

console.log(formatNumber("1234567890")) // 1,234,567,890

優(yōu)點(diǎn):把 JS 的 API 玩的了如指掌
缺點(diǎn):可能沒那么好懂,不過讀懂之后就會(huì)發(fā)出我怎么沒想到的感覺

4.3 正則版

function formatNumber(str) {
  return str.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

console.log(formatNumber("123456789")) // 1,234,567,890

下面簡單分析下正則/\B(?=(\d{3})+(?!\d))/g

  1. /\B(?=(\d{3})+(?!\d))/g:正則匹配邊界\B妄壶,邊界后面必須跟著(\d{3})+(?!\d);
  2. (\d{3})+:必須是1個(gè)或多個(gè)的3個(gè)連續(xù)數(shù)字;
  3. (?!\d):第2步中的3個(gè)數(shù)字不允許后面跟著數(shù)字;
  4. (\d{3})+(?!\d):所以匹配的邊界后面必須跟著3*n(n>=1)的數(shù)字摔握。

最終把匹配到的所有邊界換成,即可達(dá)成目標(biāo)。

優(yōu)點(diǎn):代碼少丁寄,濃縮的就是精華
缺點(diǎn):需要對(duì)正則表達(dá)式的位置匹配有一個(gè)較深的認(rèn)識(shí)氨淌,門檻大一點(diǎn)

4.4 API版

(123456789).toLocaleString('en-US')  // 1,234,567,890

如圖,你可能還不知道 JavaScripttoLocaleString 還可以這么玩伊磺。

還可以使用 Intl對(duì)象 - MDN

Intl 對(duì)象是 ECMAScript 國際化 API 的一個(gè)命名空間盛正,它提供了精確的字符串對(duì)比,數(shù)字格式化屑埋,日期和時(shí)間格式化蛮艰。Collator,NumberFormat 和 DateTimeFormat 對(duì)象的構(gòu)造函數(shù)是 Intl 對(duì)象的屬性雀彼。

new Intl.NumberFormat().format(1234567890) // 1,234,567,890

優(yōu)點(diǎn):簡單粗暴,直接調(diào)用 API
缺點(diǎn):Intl兼容性不太好即寡,不過 toLocaleString的話 IE6 都支持

你可能不知道的前端知識(shí)點(diǎn):Intl對(duì)象 和 toLocaleString的方法徊哑。

5、交換兩個(gè)整數(shù)

let a = 3,b = 4 變成 a = 4, b = 3

5.1 普通版

首先最常規(guī)的辦法聪富,引入一個(gè) temp 中間變量

let a = 3,b = 4
let temp = a
a = b
b = temp
console.log(a, b)

優(yōu)點(diǎn):一眼能看懂就是最好的優(yōu)點(diǎn)
缺點(diǎn):硬說缺點(diǎn)就是引入了一個(gè)多余的變量

5.2 進(jìn)階版

在不引入中間變量的情況下也能交互兩個(gè)變量

let a = 3,b = 4
a += b
b = a - b
a -= b
console.log(a, b)

優(yōu)點(diǎn):比起樓上那種沒有引入多余的變量莺丑,比上面那一種稍微巧妙一點(diǎn)
缺點(diǎn):當(dāng)然缺點(diǎn)也很明顯,整型數(shù)據(jù)溢出墩蔓,比如說對(duì)于32位字符最大表示有符號(hào)數(shù)字是2147483647梢莽,也就是Math.pow(2,31)-1,如果是2147483645和2147483646交換就失敗了奸披。

5.3 終極版

利用一個(gè)數(shù)異或本身等于0和異或運(yùn)算符合交換率昏名。

let a = 3,b = 4
  a ^= b
  b ^= a
  a ^= b
console.log(a, b)

下面用豎式進(jìn)行簡單說明:(10進(jìn)制化為二進(jìn)制)

    a = 011
(^) b = 100
則  a = 111(a ^ b的結(jié)果賦值給a,a已變成了7)
(^) b = 100
則  b = 011(b^a的結(jié)果賦給b阵面,b已經(jīng)變成了3)
(^) a = 111
則  a = 100(a^b的結(jié)果賦給a轻局,a已經(jīng)變成了4)

從上面的豎式可以清楚的看到利用異或運(yùn)算實(shí)現(xiàn)兩個(gè)值交換的基本過程洪鸭。

下面從深層次剖析一下:

1.對(duì)于開始的兩個(gè)賦值語句,a = a ^ b仑扑,b = b ^ a览爵,相當(dāng)于b = b ^ (a ^ b) = a ^ b ^ b,而b ^ b 顯然等于0镇饮。因此b = a ^ 0蜓竹,顯然結(jié)果為a。

  1. 同理可以分析第三個(gè)賦值語句储藐,a = a ^ b = (a ^ b) ^ a = b

注:

  1. ^ 即”異或“運(yùn)算符俱济。

它的意思是判斷兩個(gè)相應(yīng)的位值是否為”異“,為”異"(值不同)就取真(1);否則為假(0)邑茄。

  1. ^ 運(yùn)算符的特點(diǎn)是與0異或姨蝴,保持原值;與本身異或肺缕,結(jié)果為0左医。

優(yōu)點(diǎn):不存在引入中間變量,不存在整數(shù)溢出
缺點(diǎn):前端對(duì)位操作這一塊可能了解不深同木,不容易理解

5.4 究極版

熟悉 ES6 語法的人當(dāng)然不會(huì)對(duì)解構(gòu)陌生

var a = 3,b = 4;
[b, a] = [a, b]

其中的解構(gòu)的原理浮梢,我暫時(shí)還沒讀過 ES6的規(guī)范,不知道具體的細(xì)則彤路,不過我們可以看看 babel 是自己編譯的秕硝,我們可以看出點(diǎn)門路。

哈哈洲尊,簡單粗暴远豺,不知道有沒有按照 ES 的規(guī)范,其實(shí)可以扒一扒 v8的源碼坞嘀,chrome 已經(jīng)實(shí)現(xiàn)這種解構(gòu)用法躯护。

這個(gè)例子和前面的例子編寫風(fēng)格有何不同,你如果細(xì)心的話就會(huì)發(fā)現(xiàn)這兩行代碼多了一個(gè)分號(hào)丽涩,對(duì)于我這種編碼不寫分號(hào)的潔癖者棺滞,為什么加一個(gè)分號(hào)在這里,其實(shí)是有原因的矢渊,這里就簡單普及一下继准,建議大家還是寫代碼寫上分號(hào)

5.4 ECMAScript 自動(dòng)分號(hào);插入(作為補(bǔ)充矮男,防止大家以后踩坑)

盡管 JavaScript 有 C 的代碼風(fēng)格移必,但是它不強(qiáng)制要求在代碼中使用分號(hào),實(shí)際上可以省略它們昂灵。

JavaScript 不是一個(gè)沒有分號(hào)的語言避凝,恰恰相反上它需要分號(hào)來就解析源代碼舞萄。 因此 JavaScript 解析器在遇到由于缺少分號(hào)導(dǎo)致的解析錯(cuò)誤時(shí),會(huì)自動(dòng)在源代碼中插入分號(hào)管削。

5.4.1例子
var foo = function() {
} // 解析錯(cuò)誤倒脓,分號(hào)丟失
test()

自動(dòng)插入分號(hào),解析器重新解析含思。

var foo = function() {
}; // 沒有錯(cuò)誤崎弃,解析繼續(xù)
test()
5.4.2工作原理

下面的代碼沒有分號(hào),因此解析器需要自己判斷需要在哪些地方插入分號(hào)含潘。

(function(window, undefined) {
    function test(options) {
        log('testing!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}
})(window)

下面是解析器"猜測"的結(jié)果饲做。

(function(window, undefined) {
    function test(options) {

        // 沒有插入分號(hào),兩行被合并為一行
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- 插入分號(hào)

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        ); // <- 插入分號(hào)

        return; // <- 插入分號(hào), 改變了 return 表達(dá)式的行為
        { // 作為一個(gè)代碼段處理
            foo: function() {}
        }; // <- 插入分號(hào)
    }
    window.test = test; // <- 插入分號(hào)

// 兩行又被合并了
})(window)(function(window) {
    window.someLibrary = {}; // <- 插入分號(hào)
})(window); //<- 插入分號(hào)

解析器顯著改變了上面代碼的行為遏弱,在另外一些情況下也會(huì)做出錯(cuò)誤的處理盆均。

5.4.3 ECMAScript對(duì)自動(dòng)分號(hào)插入的規(guī)則

我們翻到7.9章節(jié),看看其中插入分號(hào)的機(jī)制和原理漱逸,清楚只寫以后就可以盡量以后少踩坑

必須用分號(hào)終止某些 ECMAScript 語句 ( 空語句 , 變量聲明語句 , 表達(dá)式語句 , do-while 語句 , continue 語句 , break 語句 , return 語句 ,throw 語句 )泪姨。這些分號(hào)總是明確的顯示在源文本里。然而饰抒,為了方便起見肮砾,某些情況下這些分號(hào)可以在源文本里省略。描述這種情況會(huì)說:這種情況下給源代碼的 token 流自動(dòng)插入分號(hào)袋坑。


還是比較抽象仗处,看不太懂是不是,不要緊枣宫,我們看看實(shí)際例子婆誓,總結(jié)出幾個(gè)規(guī)律就行,我們先不看抽象的也颤,看著頭暈旷档,看看具體的總結(jié)說明, 化抽象為具體 歇拆。

首先這些規(guī)則是基于兩點(diǎn):

  1. 以換行為基礎(chǔ);
  2. 解析器會(huì)盡量將新行并入當(dāng)前行范咨,當(dāng)且僅當(dāng)符合ASI規(guī)則時(shí)才會(huì)將新行視為獨(dú)立的語句故觅。
5.4.3.1 ASI的規(guī)則

1. 新行并入當(dāng)前行將構(gòu)成非法語句,自動(dòng)插入分號(hào)渠啊。

if(1 < 10) a = 1
console.log(a)
// 等價(jià)于
if(1 < 10) a = 1;
console.log(a);

2. 在continue,return,break,throw后自動(dòng)插入分號(hào)

return
{a: 1}
// 等價(jià)于
return;
{a: 1};

3. ++输吏、--后綴表達(dá)式作為新行的開始,在行首自動(dòng)插入分號(hào)

a
++
c
// 等價(jià)于
a;
++c;

4. 代碼塊的最后一個(gè)語句會(huì)自動(dòng)插入分號(hào)

function(){ a = 1 }
// 等價(jià)于
function(){ a = 1; }
5.4.3.2 No ASI的規(guī)則

1. 新行以 ( 開始

var a = 1
var b = a
(a+b).toString()
// 會(huì)被解析為以a+b為入?yún)⒄{(diào)用函數(shù)a替蛉,然后調(diào)用函數(shù)返回值的toString函數(shù)
var a = 1
var b =a(a+b).toString()

2. 新行以 [ 開始

var a = ['a1', 'a2']
var b = a
[0,1].slice(1)
// 會(huì)被解析先獲取a[1]贯溅,然后調(diào)用a[1].slice(1)拄氯。
// 由于逗號(hào)位于[]內(nèi),且不被解析為數(shù)組字面量它浅,而被解析為運(yùn)算符译柏,而逗號(hào)運(yùn)算符會(huì)先執(zhí)
行左側(cè)表達(dá)式,然后執(zhí)行右側(cè)表達(dá)式并且以右側(cè)表達(dá)式的計(jì)算結(jié)果作為返回值
var a = ['a1', 'a2']
var b = a[0,1].slice(1)

3. 新行以 / 開始

var a = 1
var b = a
/test/.test(b)
// /會(huì)被解析為整除運(yùn)算符姐霍,而不是正則表達(dá)式字面量的起始符號(hào)鄙麦。瀏覽器中會(huì)報(bào)test前多了個(gè).號(hào)
var a = 1
var b = a / test / .test(b)

4. 新行以 + 、 - 镊折、 % 和 * 開始

var a = 2
var b = a
+a
// 會(huì)解析如下格式
var a = 2
var b = a + a

5. 新行以 , 或 . 開始

var a = 2
var b = a
.toString()
console.log(typeof b)
// 會(huì)解析為
var a = 2
var b = a.toString()
console.log(typeof b)

到這里我們已經(jīng)對(duì)ASI的規(guī)則有一定的了解了胯府,另外還有一樣有趣的事情,就是“空語句”恨胚。

// 三個(gè)空語句
;;;

// 只有if條件語句骂因,語句塊為空語句。
// 可實(shí)現(xiàn)unless條件語句的效果
if(1>2);else
  console.log('2 is greater than 1 always!');

// 只有while條件語句赃泡,循環(huán)體為空語句寒波。
var a = 1
while(++a < 100);
5.4.4 結(jié)論

建議絕對(duì)不要省略分號(hào),同時(shí)也提倡將花括號(hào)和相應(yīng)的表達(dá)式放在一行急迂, 對(duì)于只有一行代碼的 if 或者 else 表達(dá)式影所,也不應(yīng)該省略花括號(hào)。 這些良好的編程習(xí)慣不僅可以提到代碼的一致性僚碎,而且可以防止解析器改變代碼行為的錯(cuò)誤處理猴娩。
關(guān)于JavaScript 語句后應(yīng)該加分號(hào)么?(點(diǎn)我查看)我們可以看看知乎上大牛們對(duì)著個(gè)問題的看法勺阐。

你可能不知道的前端知識(shí)點(diǎn):原來 JavaScript 還有位操作以及分號(hào)的使用細(xì)則

6卷中、將 argruments 對(duì)象(類數(shù)組)轉(zhuǎn)換成數(shù)組

{0:1,1:2,2:3,length:3}這種形式的就屬于類數(shù)組,就是按照數(shù)組下標(biāo)排序的對(duì)象渊抽,還有一個(gè) length屬性蟆豫,有時(shí)候我們需要這種對(duì)象能調(diào)用數(shù)組下的一個(gè)方法,這時(shí)候就需要把把類數(shù)組轉(zhuǎn)化成真正的數(shù)組懒闷。

6.1 普通版

var makeArray = function(array) {
  var ret = []
  if (array != null) {
    var i = array.length
    if (i == null || typeof array === "string") ret[0] = array
    else while (i) ret[--i] = array[i];
  }
  return ret
}
makeArray({0:1,1:2,2:3,length:3}) //[1,2,3]

優(yōu)點(diǎn):通用版本十减,沒有任何兼容性問題
缺點(diǎn):太普通

6.2 進(jìn)階版

var arr = Array.prototype.slice.call(arguments);

這種應(yīng)該是大家用過最常用的方法,至于為什么可以這么用愤估,很多人估計(jì)也是一知半解帮辟,反正我看見大家這么用我也這么用,要搞清為什么里面的原因玩焰,我們還是從規(guī)范和源碼說起由驹。

照著規(guī)范的流程,自己看看推演一下就明白了:
英文版15.4.4.10 Array.prototype.slice (start, end)
中文版15.4.4.10 Array.prototype.slice (start, end)
如果你想知道 JavaScriptsort 排序的機(jī)制昔园,到底是哪種排序好蔓榄,用的哪種并炮,也可以從規(guī)范看出端倪。

在官方的解釋中甥郑,如[mdn]

The slice() method returns a shallow copy of a portion of an array into a new array object.

簡單的說就是根據(jù)參數(shù)逃魄,返回?cái)?shù)組的一部分的 copy。所以了解其內(nèi)部實(shí)現(xiàn)才能確定它是如何工作的壹若。所以查看 V8 源碼中的 Array.js 可以看到如下的代碼:

方法 ArraySlice嗅钻,源碼地址,第 660 行,直接添加到 Array.prototype 上的“入口”店展,內(nèi)部經(jīng)過參數(shù)养篓、類型等等的判斷處理,分支為 SparseSliceSimpleSlice 處理赂蕴。

slice.call 的作用原理就是柳弄,利用 call,將 slice 的方法作用于 arrayLike概说,slice 的兩個(gè)參數(shù)為空碧注,slice 內(nèi)部解析使得 arguments.lengt 等于0的時(shí)候 相當(dāng)于處理 slice(0) : 即選擇整個(gè)數(shù)組,slice 方法內(nèi)部沒有強(qiáng)制判斷必須是 Array 類型糖赔,slice 返回的是新建的數(shù)組(使用循環(huán)取值)”萍丐,所以這樣就實(shí)現(xiàn)了類數(shù)組到數(shù)組的轉(zhuǎn)化,call 這個(gè)神奇的方法放典、slice 的處理缺一不可逝变。

直接看 slice 怎么實(shí)現(xiàn)的吧。其實(shí)就是將 array-like 對(duì)象通過下標(biāo)操作放進(jìn)了新的 Array 里面:

// This will work for genuine arrays, array-like objects, 
    // NamedNodeMap (attributes, entities, notations),
    // NodeList (e.g., getElementsByTagName), HTMLCollection (e.g., childNodes),
    // and will not fail on other DOM objects (as do DOM elements in IE < 9)
    Array.prototype.slice = function(begin, end) {
      // IE < 9 gets unhappy with an undefined end argument
      end = (typeof end !== 'undefined') ? end : this.length;

      // For native Array objects, we use the native slice function
      if (Object.prototype.toString.call(this) === '[object Array]'){
        return _slice.call(this, begin, end); 
      }

      // For array like object we handle it ourselves.
      var i, cloned = [],
        size, len = this.length;

      // Handle negative value for "begin"
      var start = begin || 0;
      start = (start >= 0) ? start : Math.max(0, len + start);

      // Handle negative value for "end"
      var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
      if (end < 0) {
        upTo = len + end;
      }

      // Actual expected size of the slice
      size = upTo - start;

      if (size > 0) {
        cloned = new Array(size);
        if (this.charAt) {
          for (i = 0; i < size; i++) {
            cloned[i] = this.charAt(start + i);
          }
        } else {
          for (i = 0; i < size; i++) {
            cloned[i] = this[start + i];
          }
        }
      }

      return cloned;
    };

優(yōu)點(diǎn):最常用的版本奋构,兼容性較強(qiáng)
缺點(diǎn):ie 低版本壳影,無法處理 dom 集合的 slice call 轉(zhuǎn)數(shù)組。(雖然具有數(shù)值鍵值弥臼、length 符合ArrayLike 的定義宴咧,卻報(bào)錯(cuò))搜索資料得到 :因?yàn)?ie 下的 dom 對(duì)象是以 com 對(duì)象的形式實(shí)現(xiàn)的,js 對(duì)象與com對(duì)象不能進(jìn)行轉(zhuǎn)換 径缅。

6.3 ES6 版本

使用 Array.from, 值需要對(duì)象有 length 屬性, 就可以轉(zhuǎn)換成數(shù)組

var arr = Array.from(arguments);

擴(kuò)展運(yùn)算符

var args = [...arguments];

ES6 中的擴(kuò)展運(yùn)算符...也能將某些數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換成數(shù)組掺栅,這種數(shù)據(jù)結(jié)構(gòu)必須有便利器接口。
優(yōu)點(diǎn):直接使用內(nèi)置 API纳猪,簡單易維護(hù)
缺點(diǎn):兼容性柿冲,使用 babel 的 profill 轉(zhuǎn)化可能使代碼變多,文件包變大

你可能不知道的前端知識(shí)點(diǎn):slice 方法的具體原理

7兆旬、數(shù)字取整 2.33333 => 2

7.1 普通版

const a = parseInt(2.33333)

parseInt() 函數(shù)解析一個(gè)字符串參數(shù),并返回一個(gè)指定基數(shù)的整數(shù) (數(shù)學(xué)系統(tǒng)的基礎(chǔ))怎栽。這個(gè)估計(jì)是直接取整最常用的方法了丽猬。
更多關(guān)于 parseInt() 函數(shù)可以查看 MDN 文檔

7.2 進(jìn)階版

const a = Math.trunc(2.33333)

Math.trunc() 方法會(huì)將數(shù)字的小數(shù)部分去掉宿饱,只保留整數(shù)部分。
特別要注意的是:Internet Explorer 不支持這個(gè)方法脚祟,不過寫個(gè) Polyfill 也很簡單:

Math.trunc = Math.trunc || function(x) {
  if (isNaN(x)) {
    return NaN;
  }
  if (x > 0) {
    return Math.floor(x);
  }
  return Math.ceil(x);
};

數(shù)學(xué)的事情還是用數(shù)學(xué)方法來處理比較好谬以。

7.3 黑科技版

7.3.1 ~~number

雙波浪線 ~~ 操作符也被稱為“雙按位非”操作符。你通秤勺溃可以使用它作為代替 Math.trunc() 的更快的方法为黎。

console.log(~~47.11)  // -> 47
console.log(~~1.9999) // -> 1
console.log(~~3)      // -> 3
console.log(~~[])     // -> 0
console.log(~~NaN)    // -> 0
console.log(~~null)   // -> 0

失敗時(shí)返回0,這可能在解決 Math.trunc() 轉(zhuǎn)換錯(cuò)誤返回 NaN 時(shí)是一個(gè)很好的替代。
但是當(dāng)數(shù)字范圍超出 ±2^31?1 即:2147483647 時(shí)行您,異常就出現(xiàn)了:

// 異常情況
console.log(~~2147493647.123) // -> -2147473649 ??
7.3.2 number | 0

| (按位或) 對(duì)每一對(duì)比特位執(zhí)行或(OR)操作铭乾。

console.log(20.15|0);          // -> 20
console.log((-20.15)|0);       // -> -20
console.log(3000000000.15|0);  // -> -1294967296 ??
7.3.3 number ^ 0

^ (按位異或),對(duì)每一對(duì)比特位執(zhí)行異或(XOR)操作娃循。

console.log(20.15^0);          // -> 20
console.log((-20.15)^0);       // -> -20
console.log(3000000000.15^0);  // -> -1294967296 ??
7.3.4 number << 0

<< (左移) 操作符會(huì)將第一個(gè)操作數(shù)向左移動(dòng)指定的位數(shù)炕檩。向左被移出的位被丟棄,右側(cè)用 0 補(bǔ)充捌斧。

console.log(20.15 < < 0);     // -> 20
console.log((-20.15) < < 0);  //-20
console.log(3000000000.15 << 0);  // -> -1294967296 ??

上面這些按位運(yùn)算符方法執(zhí)行很快笛质,當(dāng)你執(zhí)行數(shù)百萬這樣的操作非常適用,速度明顯優(yōu)于其他方法捞蚂。但是代碼的可讀性比較差妇押。還有一個(gè)特別要注意的地方,處理比較大的數(shù)字時(shí)(當(dāng)數(shù)字范圍超出 ±2^31?1 即:2147483647)姓迅,會(huì)有一些異常情況敲霍。使用的時(shí)候明確的檢查輸入數(shù)值的范圍。

8队贱、數(shù)組求和

8.1 普通版

let arr = [1, 2, 3, 4, 5]
function sum(arr){
    let x = 0
    for(let i = 0; i < arr.length; i++){
        x += arr[i]
    }
    return x
}
sum(arr) // 15

優(yōu)點(diǎn):通俗易懂色冀,簡單粗暴
缺點(diǎn):沒有亮點(diǎn),太通俗

8.2 優(yōu)雅版

let arr = [1, 2, 3, 4, 5]
function sum(arr) {
return arr.reduce((a, b) => a + b)
}
sum(arr) //15

優(yōu)點(diǎn):簡單明了柱嫌,數(shù)組迭代器方式清晰直觀
缺點(diǎn):不兼容 IE 9以下瀏覽器

8.3 終極版

let arr = [1, 2, 3, 4, 5]
function sum(arr) {
return eval(arr.join("+"))
}
sum(arr) //15

優(yōu)點(diǎn):讓人一時(shí)看不懂的就是"好方法"锋恬。
缺點(diǎn):

eval 不容易調(diào)試。用 chromeDev 等調(diào)試工具無法打斷點(diǎn)調(diào)試编丘,所以麻煩的東西也是不推薦使用的…

性能問題与学,在舊的瀏覽器中如果你使用了eval,性能會(huì)下降10倍嘉抓。在現(xiàn)代瀏覽器中有兩種編譯模式:fast path和slow path索守。fast path是編譯那些穩(wěn)定和可預(yù)測(stable and predictable)的代碼。而明顯的抑片,eval 不可預(yù)測卵佛,所以將會(huì)使用 slow path ,所以會(huì)慢。

更多關(guān)于 eval 的探討可以關(guān)注這篇文章: JavaScript 為什么不推薦使用 eval截汪?

你可能不知道的前端知識(shí)點(diǎn):eval的使用細(xì)則

最后

祝大家圣誕快樂??疾牲,歡迎補(bǔ)充和交流。



?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衙解,一起剝皮案震驚了整個(gè)濱河市阳柔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚓峦,老刑警劉巖舌剂,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異暑椰,居然都是意外死亡霍转,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門干茉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谴忧,“玉大人,你說我怎么就攤上這事角虫≌次剑” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵戳鹅,是天一觀的道長均驶。 經(jīng)常有香客問我,道長枫虏,這世上最難降的妖魔是什么妇穴? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮隶债,結(jié)果婚禮上腾它,老公的妹妹穿的比我還像新娘。我一直安慰自己死讹,他們只是感情好瞒滴,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赞警,像睡著了一般妓忍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上愧旦,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天世剖,我揣著相機(jī)與錄音,去河邊找鬼笤虫。 笑死旁瘫,一個(gè)胖子當(dāng)著我的面吹牛祖凫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播酬凳,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼蝙场,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了粱年?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤罚拟,失蹤者是張志新(化名)和其女友劉穎台诗,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赐俗,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拉队,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阻逮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粱快。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖叔扼,靈堂內(nèi)的尸體忽然破棺而出事哭,到底是詐尸還是另有隱情,我是刑警寧澤瓜富,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布鳍咱,位于F島的核電站,受9級(jí)特大地震影響与柑,放射性物質(zhì)發(fā)生泄漏谤辜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一价捧、第九天 我趴在偏房一處隱蔽的房頂上張望丑念。 院中可真熱鬧,春花似錦结蟋、人聲如沸脯倚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挠将。三九已至,卻和暖如春编整,著一層夾襖步出監(jiān)牢的瞬間舔稀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工掌测, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留内贮,地道東北人产园。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像夜郁,于是被迫代替她去往敵國和親什燕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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