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