您好,登錄后才能下訂單哦!
JS 中分為七種內置類型,七種內置類型又分為兩大類型:基本類型和對象(Object)。
基本類型有六種: null
,undefined
,boolean
,number
,string
,symbol
。
其中 JS 的數字類型是浮點類型的,沒有整型。并且浮點類型基于 IEEE 754標準實現,在使用中會遇到某些 Bug。NaN
也屬于 number
類型,并且 NaN
不等于自身。
對于基本類型來說,如果使用字面量的方式,那么這個變量只是個字面量,只有在必要的時候才會轉換為對應的類型
let a = 111 // 這只是字面量,不是 number 類型a.toString() // 使用時候才會轉換為對象類型
對象(Object)是引用類型,在使用過程中會遇到淺拷貝和深拷貝的問題。
let a = { name: 'FE' }let b = a b.name = 'EF'console.log(a.name) // EF
typeof
對于基本類型,除了 null
都可以顯示正確的類型
typeof 1 // 'number'typeof '1' // 'string'typeof undefined // 'undefined'typeof true // 'boolean'typeof Symbol() // 'symbol'typeof b // b 沒有聲明,但是還會顯示 undefined
typeof
對于對象,除了函數都會顯示 object
typeof [] // 'object'typeof {} // 'object'typeof console.log // 'function'
對于 null
來說,雖然它是基本類型,但是會顯示 object
,這是一個存在很久了的 Bug
typeof null // 'object'
PS:為什么會出現這種情況呢?因為在 JS 的最初版本中,使用的是 32 位系統,為了性能考慮使用低位存儲了變量的類型信息,000
開頭代表是對象,然而 null
表示為全零,所以將它錯誤的判斷為 object
。雖然現在的內部類型判斷代碼已經改變了,但是對于這個 Bug 卻是一直流傳下來。
如果我們想獲得一個變量的正確類型,可以通過 Object.prototype.toString.call(xx)
。這樣我們就可以獲得類似 [Object Type]
的字符串。
let a// 我們也可以這樣判斷 undefineda === undefined// 但是 undefined 保留字,能夠在低版本瀏覽器被賦值let undefined = 1// 這樣判斷就會出錯// 所以可以用下面的方式來判斷,并且代碼量更少// 因為 void 后面隨便跟上一個組成表達式// 返回就是 undefineda === void 0
除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其他所有值都轉為 true
,包括所有對象。
對象在轉換基本類型時,首先會調用 valueOf
然后調用 toString
。并且這兩個方法你是可以重寫的。
let a = { valueOf() { return 0 }}
只有當加法運算時,其中一方是字符串類型,就會把另一個也轉為字符串類型。其他運算只要其中一方是數字,那么另一方就轉為數字。并且加法運算會觸發三種類型轉換:將值轉換為原始值,轉換為數字,轉換為字符串。
1 + '1' // '11'2 * '2' // 4[1, 2] + [2, 1] // '1,22,1'// [1, 2].toString() -> '1,2'// [2, 1].toString() -> '2,1'// '1,2' + '2,1' = '1,22,1'
對于加號需要注意這個表達式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"// 因為 + 'b' -> NaN// 你也許在一些代碼中看到過 + '1' -> 1
==
操作符上圖中的 toPrimitive
就是對象轉基本類型。
一般推薦使用 ===
判斷兩個值,但是你如果想知道一個值是不是 null
,你可以通過 xx == null
來比較。
這里來解析一道題目 [] == ![] // -> true
,下面是這個表達式為何為 true
的步驟
// [] 轉成 true,然后取反變成 false[] == false// 根據第 8 條得出[] == ToNumber(false)[] == 0// 根據第 10 條得出ToPrimitive([]) == 0// [].toString() -> '''' == 0// 根據第 6 條得出0 == 0 // -> true
如果是對象,就通過 toPrimitive
轉換對象
如果是字符串,就通過 unicode
字符索引來比較
每個函數都有 prototype
屬性,除了 Function.prototype.bind()
,該屬性指向原型。
每個對象都有 __proto__
屬性,指向了創建該對象的構造函數的原型。其實這個屬性指向了 [[prototype]]
,但是 [[prototype]]
是內部屬性,我們并不能訪問到,所以使用 _proto_
來訪問。
對象可以通過 __proto__
來尋找不屬于該對象的屬性,__proto__
將對象連接起來組成了原型鏈。
如果你想更進一步的了解原型,可以仔細閱讀 深度解析原型中的各個難點。
新生成了一個對象
鏈接到原型
綁定 this
返回新對象
在調用 new
的過程中會發生以上四件事情,我們也可以試著來自己實現一個 new
function create() { // 創建一個空的對象 let obj = new Object() // 獲得構造函數 let Con = [].shift.call(arguments) // 鏈接到原型 obj.__proto__ = Con.prototype // 綁定 this,執行構造函數 let result = Con.apply(obj, arguments) // 確保 new 出來的是個對象 return typeof result === 'object' ? result : obj}
對于實例對象來說,都是通過 new
產生的,無論是 function Foo()
還是 let a = { b : 1 }
。
對于創建一個對象來說,更推薦使用字面量的方式創建對象(無論性能上還是可讀性)。因為你使用 new Object()
的方式創建對象需要通過作用域鏈一層層找到 Object
,但是你使用字面量的方式就沒這個問題。
function Foo() {}// function 就是個語法糖// 內部等同于 new Function()let a = { b: 1 }// 這個字面量內部也是使用了 new Object()
對于 new
來說,還需要注意下運算符優先級。
function Foo() { return this;}Foo.getName = function () { console.log('1');};Foo.prototype.getName = function () { console.log('2');};new Foo.getName(); // -> 1new Foo().getName(); // -> 2
從上圖可以看出,new Foo()
的優先級大于 new Foo
,所以對于上述代碼來說可以這樣劃分執行順序
new (Foo.getName()); (new Foo()).getName();
對于第一個函數來說,先執行了 Foo.getName()
,所以結果為 1;對于后者來說,先執行 new Foo()
產生了一個實例,然后通過原型鏈找到了 Foo
上的 getName
函數,所以結果為 2。
instanceof
可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype
。
我們也可以試著實現一下 instanceof
function instanceof(left, right) { // 獲得類型的原型 let prototype = right.prototype // 獲得對象的原型 left = left.__proto__ // 判斷對象的類型是否等于類型的原型 while (true) { if (left === null) return false if (prototype === left) return true left = left.__proto__ }}
this
是很多人會混淆的概念,但是其實他一點都不難,你只需要記住幾個規則就可以了。
function foo() { console.log(this.a)}var a = 2foo()var obj = { a: 2, foo: foo}obj.foo()// 以上兩者情況 `this` 只依賴于調用函數前的對象,優先級是第二個情況大于第一個情況// 以下情況是優先級最高的,`this` 只會綁定在 `c` 上,不會被任何方式修改 `this` 指向var c = new foo()c.a = 3console.log(c.a)// 還有種就是利用 call,apply,bind 改變 this,這個優先級僅次于 new
以上幾種情況明白了,很多代碼中的 this
應該就沒什么問題了,下面讓我們看看箭頭函數中的 this
function a() { return () => { return () => { console.log(this) } }}console.log(a()()())
箭頭函數其實是沒有 this
的,這個函數中的 this
只取決于他外面的第一個不是箭頭函數的函數的 this
。在這個例子中,因為調用 a
符合前面代碼中的第一個情況,所以 this
是 window
。并且 this
一旦綁定了上下文,就不會被任何代碼改變。
當執行 JS 代碼時,會產生三種執行上下文
全局執行上下文
函數執行上下文
eval 執行上下文
每個執行上下文中都有三個重要的屬性
變量對象(VO),包含變量、函數聲明和函數的形參,該屬性只能在全局上下文中訪問
作用域鏈(JS 采用詞法作用域,也就是說變量的作用域是在定義時就決定了)
this
var a = 10function foo(i) { var b = 20}foo()
對于上述代碼,執行棧中有兩個上下文:全局上下文和函數 foo
上下文。
stack = [ globalContext, fooContext]
對于全局上下文來說,VO 大概是這樣的
globalContext.VO === globe globalContext.VO = { a: undefined, foo: <Function>,}
對于函數 foo
來說,VO 不能訪問,只能訪問到活動對象(AO)
fooContext.VO === foo.AOfooContext.AO { i: undefined, b: undefined, arguments: <>}// arguments 是函數獨有的對象(箭頭函數沒有)// 該對象是一個偽數組,有 `length` 屬性且可以通過下標訪問元素// 該對象中的 `callee` 屬性代表函數本身// `caller` 屬性代表函數的調用者
對于作用域鏈,可以把它理解成包含自身變量對象和上級變量對象的列表,通過 [[Scope]]
屬性查找上級變量
fooContext.[[Scope]] = [ globalContext.VO]fooContext.Scope = fooContext.[[Scope]] + fooContext.VOfooContext.Scope = [ fooContext.VO, globalContext.VO]
接下來讓我們看一個老生常談的例子,var
b() // call bconsole.log(a) // undefinedvar a = 'Hello world'function b() { console.log('call b')}
想必以上的輸出大家肯定都已經明白了,這是因為函數和變量提升的原因。通常提升的解釋是說將聲明的代碼移動到了頂部,這其實沒有什么錯誤,便于大家理解。但是更準確的解釋應該是:在生成執行上下文時,會有兩個階段。第一個階段是創建的階段(具體步驟是創建 VO),JS 解釋器會找出需要提升的變量和函數,并且給他們提前在內存中開辟好空間,函數的話會將整個函數存入內存中,變量只聲明并且賦值為 undefined,所以在第二個階段,也就是代碼執行階段,我們可以直接提前使用。
在提升的過程中,相同的函數會覆蓋上一個函數,并且函數優先于變量提升
b() // call b secondfunction b() { console.log('call b fist')}function b() { console.log('call b second')}var b = 'Hello world'
var
會產生很多錯誤,所以在 ES6中引入了 let
。let
不能在聲明前使用,但是這并不是常說的 let
不會提升,let
提升了聲明但沒有賦值,因為臨時死區導致了并不能在聲明前使用。
對于非匿名的立即執行函數需要注意以下一點
var foo = 1(function foo() { foo = 10 console.log(foo)}()) // -> ? foo() { foo = 10 ; console.log(foo) }
因為當 JS 解釋器在遇到非匿名的立即執行函數時,會創建一個輔助的特定對象,然后將函數名稱作為這個對象的屬性,因此函數內部才可以訪問到 foo
,但是這又個值是只讀的,所以對它的賦值并不生效,所以打印的結果還是這個函數,并且外部的值也沒有發生更改。
specialObject = {}; Scope = specialObject + Scope; foo = new FunctionExpression;foo.[[Scope]] = Scope;specialObject.foo = foo; // {DontDelete}, {ReadOnly} delete Scope[0]; // remove specialObject from the front of scope chain
閉包的定義很簡單:函數 A 返回了一個函數 B,并且函數 B 中使用了函數 A 的變量,函數 B 就被稱為閉包。
function A() { let a = 1 function B() { console.log(a) } return B}
你是否會疑惑,為什么函數 A 已經彈出調用棧了,為什么函數 B 還能引用到函數 A 中的變量。因為函數 A 中的變量這時候是存儲在堆上的。現在的 JS 引擎可以通過逃逸分析辨別出哪些變量需要存儲在堆上,哪些需要存儲在棧上。
經典面試題,循環中使用閉包解決 var
定義函數的問題
for ( var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }?
首先因為 setTimeout
是個異步函數,所有會先把循環全部執行完畢,這時候 i
就是 6 了,所以會輸出一堆 6。
解決辦法兩種,第一種使用閉包
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i);}
第二種就是使用 setTimeout
的第三個參數
for ( var i=1; i<=5; i++) { setTimeout( function timer(j) { console.log( j ); }, i*1000, i);}
因為對于 let
來說
第三種就是使用 let
定義 i
了
for ( let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 );}
因為對于 let
來說,他會創建一個塊級作用域,相當于
{ // 形成塊級作用域 let i = 0 { let ii = i setTimeout( function timer() { console.log( i ); }, i*1000 ); } i++ { let ii = i } i++ { let ii = i } ...}
let a = { age: 1}let b = a a.age = 2console.log(b.age) // 2
從上述例子中我們可以發現,如果給一個變量賦值一個對象,那么兩者的值會是同一個引用,其中一方改變,另一方也會相應改變。
通常在開發中我們不希望出現這樣的問題,我們可以使用淺拷貝來解決這個問題。
首先可以通過 Object.assign
來解決這個問題。
let a = { age: 1}let b = Object.assign({}, a)a.age = 2console.log(b.age) // 1
當然我們也可以通過展開運算符(…)來解決
let a = { age: 1}let b = {...a}a.age = 2console.log(b.age) // 1
通常淺拷貝就能解決大部分問題了,但是當我們遇到如下情況就需要使用到深拷貝了
let a = { age: 1, jobs: { first: 'FE' }}let b = {...a}a.jobs.first = 'native'console.log(b.jobs.first) // native
淺拷貝只解決了第一層的問題,如果接下去的值中還有對象的話,那么就又回到剛開始的話題了,兩者享有相同的引用。要解決這個問題,我們需要引入深拷貝。
這個問題通常可以通過 JSON.parse(JSON.stringify(object))
來解決。
let a = { age: 1, jobs: { first: 'FE' }}let b = JSON.parse(JSON.stringify(a))a.jobs.first = 'native'console.log(b.jobs.first) // FE
但是該方法也是有局限性的:
會忽略 undefined
不能序列化函數
不能解決循環引用的對象
let obj = { a: 1, b: { c: 2, d: 3, },}obj.c = obj.b obj.e = obj.a obj.b.c = obj.c obj.b.d = obj.b obj.b.e = obj.b.clet newObj = JSON.parse(JSON.stringify(obj))console.log(newObj)
如果你有這么一個循環引用對象,你會發現你不能通過該方法深拷貝
在遇到函數或者 undefined
的時候,該對象也不能正常的序列化
let a = { age: undefined, jobs: function() {}, name: 'yck'}let b = JSON.parse(JSON.stringify(a))console.log(b) // {name: "yck"}
你會發現在上述情況中,該方法會忽略掉函數和 undefined
。
但是在通常情況下,復雜數據都是可以序列化的,所以這個函數可以解決大部分問題,并且該函數是內置函數中處理深拷貝性能最快的。當然如果你的數據中含有以上三種情況下,可以使用 loadash 的深拷貝函數。
如果你所需拷貝的對象含有內置類型并且不包含函數,可以使用 MessageChannel
function structuralClone(obj) { return new Promise(resolve => { const {port1, port2} = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); });}var obj = {a: 1, b: { c: b}}// 注意該方法是異步的// 可以處理 undefined 和循環引用對象const clone = await structuralClone(obj);
在有 Babel 的情況下,我們可以直接使用 ES6 的模塊化
// file a.jsexport function a() {}export function b() {}// file b.jsexport default function() {}import {a, b} from './a.js'import XXX from './b.js'
CommonJs
是 Node 獨有的規范,瀏覽器中使用就需要用到 Browserify
解析了。
// a.jsmodule.exports = { a: 1}// orexports.a = 1// b.jsvar module = require('./a.js')module.a // -> log 1
在上述代碼中,module.exports
和 exports
很容易混淆,讓我們來看看大致內部實現
var module = require('./a.js')module.a// 這里其實就是包裝了一層立即執行函數,這樣就不會污染全局變量了,// 重要的是 module 這里,module 是 Node 獨有的一個變量module.exports = { a: 1}// 基本實現var module = { exports: {} // exports 就是個空對象}// 這個是為什么 exports 和 module.exports 用法相似的原因var exports = module.exportsvar load = function (module) { // 導出的東西 var a = 1 module.exports = a return module.exports};
再來說說 module.exports
和 exports
,用法其實是相似的,但是不能對 exports
直接賦值,不會有任何效果。
對于 CommonJS
和 ES6 中的模塊化的兩者區別是:
前者支持動態導入,也就是 require(${path}/xx.js)
,后者目前不支持,但是已有提案
前者是同步導入,因為用于服務端,文件都在本地,同步導入即使卡住主線程影響也不大。而后者是異步導入,因為用于瀏覽器,需要下載文件,如果也采用導入會對渲染有很大影響
前者在導出時都是值拷貝,就算導出的值變了,導入的值也不會改變,所以如果想更新值,必須重新導入一次。但是后者采用實時綁定的方式,導入導出的值都指向同一個內存地址,所以導入值會跟隨導出值變化
后者會編譯成 require/exports
來執行的
AMD 是由 RequireJS
提出的
// AMDdefine(['./a', './b'], function(a, b) { a.do() b.do()})define(function(require, exports, module) { var a = require('./a') a.doSomething() var b = require('./b') b.doSomething()})
你是否在日常開發中遇到一個問題,在滾動事件中需要做個復雜計算或者實現一個按鈕的防二次點擊操作。
這些需求都可以通過函數防抖動來實現。尤其是第一個需求,如果在頻繁的事件回調中做復雜計算,很有可能導致頁面卡頓,不如將多次計算合并為一次計算,只在一個精確點做操作。因為防抖動的輪子很多,這里也不重新自己造個輪子了,直接使用 underscore 的源碼來解釋防抖動。
/** * underscore 防抖函數,返回函數連續調用時,空閑時間必須大于或等于 wait,func 才會執行 * * @param {function} func 回調函數 * @param {number} wait 表示時間窗口的間隔 * @param {boolean} immediate 設置為ture時,是否立即調用函數 * @return {function} 返回客戶調用函數 */_.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function() { // 現在和上一次時間戳比較 var last = _.now() - timestamp; // 如果當前間隔時間少于設定時間且大于0就重新設置定時器 if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { // 否則的話就是時間到了執行回調函數 timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; // 獲得時間戳 timestamp = _.now(); // 如果定時器不存在且立即執行函數 var callNow = immediate && !timeout; // 如果定時器不存在就創建一個 if (!timeout) timeout = setTimeout(later, wait); if (callNow) { // 如果需要立即執行函數的話 通過 apply 執行 result = func.apply(context, args); context = args = null; } return result; }; };
整體函數實現的不難,總結一下。
對于按鈕防點擊來說的實現:一旦我開始一個定時器,只要我定時器還在,不管你怎么點擊都不會執行回調函數。一旦定時器結束并設置為 null
,就可以再次點擊了。
對于延時執行函數來說的實現:每次調用防抖動函數都會判斷本次調用和之前的時間間隔,如果小于需要的時間間隔,就會重新創建一個定時器,并且定時器的延時為設定時間減去之前的時間間隔。一旦時間到了,就會執行相應的回調函數。
防抖動和節流本質是不一樣的。防抖動是將多次執行變為最后一次執行,節流是將多次執行變成每隔一段時間執行。
/** * underscore 節流函數,返回函數連續調用時,func 執行頻率限定為 次 / wait * * @param {function} func 回調函數 * @param {number} wait 表示時間窗口的間隔 * @param {object} options 如果想忽略開始函數的的調用,傳入{leading: false}。 * 如果想忽略結尾函數的調用,傳入{trailing: false} * 兩者不能共存,否則函數不能執行 * @return {function} 返回客戶調用函數 */_.throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 之前的時間戳 var previous = 0; // 如果 options 沒傳則設為空對象 if (!options) options = {}; // 定時器回調函數 var later = function() { // 如果設置了 leading,就將 previous 設為 0 // 用于下面函數的第一個 if 判斷 previous = options.leading === false ? 0 : _.now(); // 置空一是為了防止內存泄漏,二是為了下面的定時器判斷 timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { // 獲得當前時間戳 var now = _.now(); // 首次進入前者肯定為 true // 如果需要第一次不執行函數 // 就將上次時間戳設為當前的 // 這樣在接下來計算 remaining 的值時會大于0 if (!previous && options.leading === false) previous = now; // 計算剩余時間 var remaining = wait - (now - previous); context = this; args = arguments; // 如果當前調用已經大于上次調用時間 + wait // 或者用戶手動調了時間 // 如果設置了 trailing,只會進入這個條件 // 如果沒有設置 leading,那么第一次會進入這個條件 // 還有一點,你可能會覺得開啟了定時器那么應該不會進入這個 if 條件了 // 其實還是會進入的,因為定時器的延時 // 并不是準確的時間,很可能你設置了2秒 // 但是他需要2.2秒才觸發,這時候就會進入這個條件 if (remaining <= 0 || remaining > wait) { // 如果存在定時器就清理掉否則會調用二次回調 if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 判斷是否設置了定時器和 trailing // 沒有的話就開啟一個定時器 // 并且不能不能同時設置 leading 和 trailing timeout = setTimeout(later, remaining); } return result; }; };
在 ES5 中,我們可以使用如下方式解決繼承的問題
function Super() {}Super.prototype.getNumber = function() { return 1}function Sub() {}let s = new Sub()Sub.prototype = Object.create(Super.prototype, { constructor: { value: Sub, enumerable: false, writable: true, configurable: true }})
以上繼承實現思路就是將子類的原型設置為父類的原型
在 ES6 中,我們可以通過 class
語法輕松解決這個問題
class MyDate extends Date { test() { return this.getTime() }}let myDate = new MyDate()myDate.test()
但是 ES6 不是所有瀏覽器都兼容,所以我們需要使用 Babel 來編譯這段代碼。
如果你使用編譯過得代碼調用 myDate.test()
你會驚奇地發現出現了報錯
因為在 JS 底層有限制,如果不是由 Date
構造出來的實例的話,是不能調用 Date
里的函數的。所以這也側面的說明了:ES6 中的 class
繼承與 ES5 中的一般繼承寫法是不同的。
既然底層限制了實例必須由 Date
構造出來,那么我們可以改變下思路實現繼承
function MyData() {}MyData.prototype.test = function () { return this.getTime()}let d = new Date()Object.setPrototypeOf(d, MyData.prototype)Object.setPrototypeOf(MyData.prototype, Date.prototype)
以上繼承實現思路:先創建父類實例 => 改變實例原先的 _proto__
轉而連接到子類的 prototype
=> 子類的 prototype
的 __proto__
改為父類的 prototype
。
通過以上方法實現的繼承就可以完美解決 JS 底層的這個限制。
首先說下前兩者的區別。
call
和 apply
都是為了解決改變 this
的指向。作用都是相同的,只是傳參的方式不同。
除了第一個參數外,call
可以接收一個參數列表,apply
只接受一個參數數組。
let a = { value: 1}function getValue(name, age) { console.log(name) console.log(age) console.log(this.value)}getValue.call(a, 'yck', '24')getValue.apply(a, ['yck', '24'])
可以從以下幾點來考慮如何實現
不傳入第一個參數,那么默認為 window
改變了 this 指向,讓新的對象可以執行該函數。那么思路是否可以變成給新的對象添加一個函數,然后在執行完以后刪除?
Function.prototype.myCall = function (context) { var context = context || window // 給 context 添加一個屬性 // getValue.call(a, 'yck', '24') => a.fn = getValue context.fn = this // 將 context 后面的參數取出來 var args = [...arguments].slice(1) // getValue.call(a, 'yck', '24') => a.fn('yck', '24') var result = context.fn(...args) // 刪除 fn delete context.fn return result}
以上就是 call
的思路,apply
的實現也類似
Function.prototype.myApply = function (context) { var context = context || window context.fn = this var result // 需要判斷是否存儲第二個參數 // 如果存在,就將第二個參數展開 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result}
bind
和其他兩個方法作用也是一致的,只是該方法會返回一個函數。并且我們可以通過 bind
實現柯里化。
同樣的,也來模擬實現下 bind
Function.prototype.myBind = function (context) { if (typeof this !== 'function') { throw new TypeError('Error') } var _this = this var args = [...arguments].slice(1) // 返回一個函數 return function F() { // 因為返回了一個函數,我們可以 new F(),所以需要判斷 if (this instanceof F) { return new _this(args, ...arguments) } return _this.apply(context, args.concat(arguments)) }}
Promise 是 ES6 新增的語法,解決了回調地獄的問題。
可以把 Promise 看成一個狀態機。初始是 pending
狀態,可以通過函數 resolve
和 reject
,將狀態轉變為 resolved
或者 rejected
狀態,狀態一旦改變就不能再次變化。
then
函數會返回一個 Promise 實例,并且該返回值是一個新的實例而不是之前的實例。因為 Promise 規范規定除了 pending
狀態,其他狀態是不可以改變的,如果返回的是一個相同實例的話,多個 then
調用就失去意義了。
對于 then
來說,本質上可以把它看成是 flatMap
// 三種狀態const PENDING = "pending";const RESOLVED = "resolved";const REJECTED = "rejected";// promise 接收一個函數參數,該函數會立即執行function MyPromise(fn) { let _this = this; _this.currentState = PENDING; _this.value = undefined; // 用于保存 then 中的回調,只有當 promise // 狀態為 pending 時才會緩存,并且每個實例至多緩存一個 _this.resolvedCallbacks = []; _this.rejectedCallbacks = []; _this.resolve = function (value) { if (value instanceof MyPromise) { // 如果 value 是個 Promise,遞歸執行 return value.then(_this.resolve, _this.reject) } setTimeout(() => { // 異步執行,保證執行順序 if (_this.currentState === PENDING) { _this.currentState = RESOLVED; _this.value = value; _this.resolvedCallbacks.forEach(cb => cb()); } }) }; _this.reject = function (reason) { setTimeout(() => { // 異步執行,保證執行順序 if (_this.currentState === PENDING) { _this.currentState = REJECTED; _this.value = reason; _this.rejectedCallbacks.forEach(cb => cb()); } }) } // 用于解決以下問題 // new Promise(() => throw Error('error)) try { fn(_this.resolve, _this.reject); } catch (e) { _this.reject(e); }}MyPromise.prototype.then = function (onResolved, onRejected) { var self = this; // 規范 2.2.7,then 必須返回一個新的 promise var promise2; // 規范 2.2.onResolved 和 onRejected 都為可選參數 // 如果類型不是函數需要忽略,同時也實現了透傳 // Promise.resolve(4).then().then((value) => console.log(value)) onResolved = typeof onResolved === 'function' ? onResolved : v => v; onRejected = typeof onRejected === 'function' ? onRejected : r => throw r; if (self.currentState === RESOLVED) { return (promise2 = new MyPromise(function (resolve, reject) { // 規范 2.2.4,保證 onFulfilled,onRjected 異步執行 // 所以用了 setTimeout 包裹下 setTimeout(function () { try { var x = onResolved(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }); })); } if (self.currentState === REJECTED) { return (promise2 = new MyPromise(function (resolve, reject) { setTimeout(function () { // 異步執行onRejected try { var x = onRejected(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }); })); } if (self.currentState === PENDING) { return (promise2 = new MyPromise(function (resolve, reject) { self.resolvedCallbacks.push(function () { // 考慮到可能會有報錯,所以使用 try/catch 包裹 try { var x = onResolved(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (r) { reject(r); } }); self.rejectedCallbacks.push(function () { try { var x = onRejected(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (r) { reject(r); } }); })); }};// 規范 2.3function resolutionProcedure(promise2, x, resolve, reject) { // 規范 2.3.1,x 不能和 promise2 相同,避免循環引用 if (promise2 === x) { return reject(new TypeError("Error")); } // 規范 2.3.2 // 如果 x 為 Promise,狀態為 pending 需要繼續等待否則執行 if (x instanceof MyPromise) { if (x.currentState === PENDING) { x.then(function (value) { // 再次調用該函數是為了確認 x resolve 的 // 參數是什么類型,如果是基本類型就再次 resolve // 把值傳給下個 then resolutionProcedure(promise2, value, resolve, reject); }, reject); } else { x.then(resolve, reject); } return; } // 規范 2.3.3.3.3 // reject 或者 resolve 其中一個執行過得話,忽略其他的 let called = false; // 規范 2.3.3,判斷 x 是否為對象或者函數 if (x !== null && (typeof x === "object" || typeof x === "function")) { // 規范 2.3.3.2,如果不能取出 then,就 reject try { // 規范 2.3.3.1 let then = x.then; // 如果 then 是函數,調用 x.then if (typeof then === "function") { // 規范 2.3.3.3 then.call( x, y => { if (called) return; called = true; // 規范 2.3.3.3.1 resolutionProcedure(promise2, y, resolve, reject); }, e => { if (called) return; called = true; reject(e); } ); } else { // 規范 2.3.3.4 resolve(x); } } catch (e) { if (called) return; called = true; reject(e); } } else { // 規范 2.3.4,x 為基本類型 resolve(x); }}
以上就是根據 Promise / A+ 規范來實現的代碼,可以通過 promises-aplus-tests
的完整測試
Generator 是 ES6 中新增的語法,和 Promise 一樣,都可以用來異步編程
// 使用 * 表示這是一個 Generator 函數// 內部可以通過 yield 暫停代碼// 通過調用 next 恢復執行function* test() { let a = 1 + 2; yield 2; yield 3;}let b = test();console.log(b.next()); // > { value: 2, done: false }console.log(b.next()); // > { value: 3, done: false }console.log(b.next()); // > { value: undefined, done: true }
從以上代碼可以發現,加上 *
的函數執行后擁有了 next
函數,也就是說函數執行后返回了一個對象。每次調用 next
函數可以繼續執行被暫停的代碼。以下是 Generator 函數的簡單實現
// cb 也就是編譯過的 test 函數function generator(cb) { return (function() { var object = { next: 0, stop: function() {} }; return { next: function() { var ret = cb(object); if (ret === undefined) return { value: undefined, done: true }; return { value: ret, done: false }; } }; })();}// 如果你使用 babel 編譯后可以發現 test 函數變成了這樣function test() { var a; return generator(function(_context) { while (1) { switch ((_context.prev = _context.next)) { // 可以發現通過 yield 將代碼分割成幾塊 // 每次執行 next 函數就執行一塊代碼 // 并且表明下次需要執行哪塊代碼 case 0: a = 1 + 2; _context.next = 4; return 2; case 4: _context.next = 6; return 3; // 執行完畢 case 6: case "end": return _context.stop(); } } });}
Map
作用是生成一個新數組,遍歷原數組,將每個元素拿出來做一些變換然后 append
到新的數組中。
[1, 2, 3].map((v) => v + 1)// -> [2, 3, 4]
Map
有三個參數,分別是當前索引元素,索引,原數組
['1','2','3'].map(parseInt)// parseInt('1', 0) -> 1// parseInt('2', 1) -> NaN// parseInt('3', 2) -> NaN
FlapMap
和 map
的作用幾乎是相同的,但是對于多維數組來說,會將原數組降維。可以將 FlapMap
看成是 map
+ flatten
,目前該函數在瀏覽器中還不支持。
[1, [2], 3].flatMap((v) => v + 1)// -> [2, 3, 4]
如果想將一個多維數組徹底的降維,可以這樣實現
const flattenDeep = (arr) => Array.isArray(arr) ? arr.reduce( (a, b) => [...flattenDeep(a), ...flattenDeep(b)] , []) : [arr]flattenDeep([1, [[2], [3, [4]], 5]])
Reduce
作用是數組中的值組合起來,最終得到一個值
function a() { console.log(1);}function b() { console.log(2);}[a, b].reduce((a, b) => a(b()))// -> 2 1
一個函數如果加上 async
,那么該函數就會返回一個 Promise
async function test() { return "1";}console.log(sync()); // -> Promise {<resolved>: "1"}
可以把 async
看成將函數返回值使用 Promise.resolve()
包裹了下。
await
只能在 async
函數中使用
function sleep() { return new Promise(resolve => { setTimeout(() => { console.log('finish') resolve("sleep"); }, 2000); });}async function test() { let value = await sleep(); console.log("object");}test()
上面代碼會先打印 finish
然后再打印 object
。因為 await
會等待 sleep
函數 resolve
,所以即使后面是同步代碼,也不會先去執行同步代碼再來執行異步代碼。
async 和 await
相比直接使用 Promise
來說,優勢在于處理 then
的調用鏈,能夠更清晰準確的寫出代碼。缺點在于濫用 await
可能會導致性能問題,因為 await
會阻塞代碼,也許之后的異步代碼并不依賴于前者,但仍然需要等待前者完成,導致代碼失去了并發性。
下面來看一個使用 await
的代碼。
var a = 0var b = async () => { a = a + await 10 console.log('2', a) // -> '2' 10 a = (await 10) + a console.log('3', a) // -> '3' 20}b()a++console.log('1', a) // -> '1' 1
對于以上代碼你可能會有疑惑,這里說明下原理
首先函數 b
先執行,在執行到 await 10
之前變量 a
還是 0,因為在 await
內部實現了 generators
,generators
會保留堆棧中東西,所以這時候 a = 0
被保存了下來
因為 await
是異步操作,所以會先執行 console.log('1', a)
這時候同步代碼執行完畢,開始執行異步代碼,將保存下來的值拿出來使用,這時候 a = 10
然后后面就是常規執行代碼了
Proxy 是 ES6 中新增的功能,可以用來自定義對象中的操作
let p = new Proxy(target, handler);// `target` 代表需要添加代理的對象// `handler` 用來自定義對象中的操作
可以很方便的使用 Proxy 來實現一個數據綁定和監聽
let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { setBind(value); return Reflect.set(target, property, value); } }; return new Proxy(obj, handler);};let obj = { a: 1 }let valuelet p = onWatch(obj, (v) => { value = v}, (target, property) => { console.log(`Get '${property}' = ${target[property]}`);})p.a = 2 // bind `value` to `2`p.a // -> Get 'a' = 2
因為 JS 采用 IEEE 754 雙精度版本(64位),并且只要采用 IEEE 754 的語言都有該問題。
我們都知道計算機表示十進制是采用二進制表示的,所以 0.1
在二進制表示為
// (0011) 表示循環0.1 = 2^-4 * 1.10011(0011)
那么如何得到這個二進制的呢,我們可以來演算下
小數算二進制和整數不同。乘法計算時,只計算小數位,整數位用作每一位的二進制,并且得到的第一位為最高位。所以我們得出 0.1 = 2^-4 * 1.10011(0011)
,那么 0.2
的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)
。
回來繼續說 IEEE 754 雙精度。六十四位中符號位占一位,整數位占十一位,其余五十二位都為小數位。因為 0.1
和 0.2
都是無限循環的二進制了,所以在小數位末尾處需要判斷是否進位(就和十進制的四舍五入一樣)。
所以 2^-4 * 1.10011...001
進位后就變成了 2^-4 * 1.10011(0011 * 12次)010
。那么把這兩個二進制加起來會得出 2^-2 * 1.0011(0011 * 11次)0100
, 這個值算成十進制就是 0.30000000000000004
下面說一下原生解決辦法,如下代碼所示
parseFloat((0.1 + 0.2).toFixed(10))
元字符 | 作用 |
---|---|
. | 匹配任意字符除了換行符 |
[] | 匹配方括號內的任意字符。比如 [0-9] 就可以用來匹配任意數字 |
^ | ^9,這樣使用代表匹配以 9 開頭。[^ 9],這樣使用代表不匹配方括號內除了 9 的字符 |
{1, 2} | 匹配 1 到 2 位字符 |
(yck) | 只匹配和 yck 相同字符串 |
| | 匹配 | 前后任意字符 |
\ | 轉義 |
* | 只匹配出現 -1 次以上 * 前的字符 |
+ | 只匹配出現 0 次以上 + 前的字符 |
? | ? 之前字符可選 |
修飾語 | 作用 |
---|---|
i | 忽略大小寫 |
g | 全局搜索 |
m | 多行 |
簡寫 | 作用 |
---|---|
\w | 匹配字母數字或下劃線或漢字 |
\W | 和上面相反 |
\s | 匹配任意的空白符 |
\S | 和上面相反 |
\d | 匹配數字 |
\D | 和上面相反 |
\b | 匹配單詞的開始或結束 |
\B | 和上面相反 |
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。