內(nèi)置類型
JS中分為七種內(nèi)置類型确虱,其中內(nèi)置類型又分為兩大類型:
- 基本類型
- 對象(Object)
基本類型有六種:
- null
- undefined
- string
- number
- boolean
- symbol
其中JS的數(shù)字類型是浮點(diǎn)類型的含友,沒有整型。并且浮點(diǎn)類型基于 IEEE 754 標(biāo)準(zhǔn)實(shí)現(xiàn)校辩,在使用中會遇到某些 Bug窘问。NaN 也屬于 number 類型,并且 NaN 不等于自身宜咒。
對于基本類型來說惠赫,如果使用字面量的方式,那么這個變量只是個字面量故黑,只有在必要的時候才會轉(zhuǎn)換為對應(yīng)的類型儿咱。
let a = 111 // 這只是字面量庭砍,不是 number 類型
a.toString() // 使用的時候才會轉(zhuǎn)換為對象類型
對象(Object)是引用類型,在使用過程中會遇到淺拷貝和深拷貝的問題概疆。
let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF
Typeof
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'
類型轉(zhuǎn)換
轉(zhuǎn)Boolean
在條件判斷時嫉柴,除了 undefined、null奉呛、false计螺、NaN、''瞧壮、0登馒、-0 ,其他所有值都轉(zhuǎn)為 true 咆槽,包括所有對象陈轿。
對象轉(zhuǎn)基本類型
對象在轉(zhuǎn)換基本類型時,首先會調(diào)用 valueOf 然后調(diào)用 toString秦忿。并且這兩個方法是可以重寫的麦射。
let a = {
valueOf() {
return 0
}
}
當(dāng)然也可以重寫 Symbol.toPrimitive ,該方法在轉(zhuǎn)基本類型時調(diào)用優(yōu)先級最高灯谣。
let a = {
valueOf() {
return 0;
},
toString() {
return '1';
},
[Symbol.toPrimitive]() {
return 2;
}
}
1 + a // => 3
'1' + a // => '12'
四則運(yùn)算符
只有當(dāng)加法運(yùn)算時潜秋,其中一方是字符串類型,就會把另一個也轉(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)?+ 'b' -> NaN
== 操作符
比較運(yùn)算 x == y谬擦,其中 x 和 y 是值,產(chǎn)生 true 或者 false朽缎。這樣的比較按如下方式進(jìn)行:
1.若 Type(x) 與 Type(y) 相同惨远,則
- 若 Type(x) 為 Undefined谜悟,返回 true。
- 若 Type(x) 為Null北秽,返回 true葡幸。
- 若 Type(x) 為 Number,則
- 若 x 為NaN贺氓,返回 false蔚叨。
- 若 y 為NaN,返回 false辙培。
- 若 x 與 y 為相等數(shù)值蔑水,返回 true。
- 若 x 為 +0 且 y 為 -0扬蕊,返回 true搀别。
- 若 x 為 -0 且 y 為 +0,返回 true尾抑。
- 其它情況返回 false歇父。
- 若 Type(x) 為 String,則當(dāng) x 和 y 為完全相同的字符串序列(長度相等且相同字符在相同位置)時返回 true再愈。否則榜苫,返回 false。
- 若 Type(x) 為 Boolean践磅,當(dāng) x 和 y 同為 true 或者 同為 false 時返回 true单刁。否則,返回 false府适。
- 當(dāng) x 和 y 為引用同一對象時返回 ture羔飞。否則,返回 false檐春。
2.若 x 為 null 且 y 為 undefined逻淌,返回 true。
3.若 y 為 null 且 x 為 undefined疟暖,返回 true卡儒。
4.若 Type(x) 為 Number 且 Type(y) 為 String,返回 comparison x == ToNumber(y) 的結(jié)果俐巴。
5.若 Type(x) 為 String 且 Type(y) 為 Number骨望,返回比較 ToNumber(x) == y 的結(jié)果。
6.若 Type(x) 為 Boolean欣舵,返回比較 ToNumber(x) == y 的結(jié)果擎鸠。
7.若 Type(y) 為 Boolean,返回比較 x == ToNumber(y) 的結(jié)果缘圈。
8.若 Type(x) 為 String 或者 Number劣光,且 Type(y) 為 Object袜蚕,返回比較 x == ToPrimitive(y) 的結(jié)果。
9.若 Type(x) 為 Object 且 Type(y) 為 String 或 Number绢涡,返回比較 ToPrimitive(x) == y 的結(jié)果牲剃。
10.其他情況返回 false。
toPrimitive 就是對象轉(zhuǎn)基本類型雄可。
題目:
[ ] == ![ ] // -> true
// [] 轉(zhuǎn)成 true凿傅,然后取反變成 false
[] == false
// 根據(jù)第 7 條得出
[] == ToNumber(false)
[] == 0
// 根據(jù)第 9 條得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根據(jù)第 6 條得出
0 == 0 // true
比較運(yùn)算符
- 如果是對象,就通過 toPrimitive 轉(zhuǎn)換對象
- 如果是字符串数苫,就通過 unicode 字符索引來比較
原型
每個函數(shù)都有 prototype 屬性狭归,除了 Function.prototype.bind() ,該屬性指向原型文判。
每個對象都有 proto 屬性过椎,指向了創(chuàng)建該對象的構(gòu)造函數(shù)的原型。其實(shí)這個屬性指向了 [[prototype]] 戏仓,但是 [[prototype]] 是內(nèi)部屬性疚宇,我們并不能訪問到,所以使用 proto 來訪問赏殃。
對象可以通過 proto 來尋找不屬于該對象的屬性敷待,proto 將對象連接起來組成了原型鏈。
new
1.新生成了一個對象
2.鏈接到原型
3.綁定 this
4.返回新對象
在調(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
}
對應(yīng)實(shí)例對象來說,都是通過 new 產(chǎn)生的抗蠢,無論是 function Foo() 還是 let a = { b: 1 }举哟。
對于創(chuàng)建一個對象來說,更推薦使用字面量的方式創(chuàng)建對象(無論性能上還是可讀性)迅矛。因?yàn)槟闶褂?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(); // 1
new Foo().getName(); // 2
// 等價于
new (Foo.getName());
new (Foo()).getName();
instanceof
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
this 是很多人會混淆的概念庐椒,但是其實(shí)它一點(diǎn)也不難,你只需記住幾個規(guī)則就可以了蚂踊。
function foo() {
console.log(this.a)
}
var a = 1
foo()
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 = 3
console.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 一旦綁定了上下文,就不會被任何代碼改變吗货。
執(zhí)行上下文
當(dāng)執(zhí)行JS代碼時泳唠,會產(chǎn)生三種執(zhí)行上下文
- 全局執(zhí)行上下文
- 函數(shù)執(zhí)行上下文
- eval 執(zhí)行上下文
每個執(zhí)行上下文中都有三個重要的屬性
- 變量對象(VO),包含變量宙搬、函數(shù)聲明和函數(shù)的形參笨腥,該屬性只能在全局上下文中訪問
- 作用域鏈(JS 采用詞法作用域,也就是說變量的作用域是在定義時就決定了)
- this
var a = 10;
function 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.AO
fooContext.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.VO
fooContext.Scope = [
fooContext.VO,
globalContext.VO
]
接下來讓我們看一個老生常談的例子,var
b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b')
}
想必以上的輸出大家肯定都已經(jīng)明白了勤众,這是因?yàn)楹瘮?shù)和變量提升的原因建峭。通常提升的解釋是說將聲明的代碼移到了頂部,這其實(shí)沒有什么錯誤决摧,便于大家理解亿蒸。但是更準(zhǔn)確的解釋應(yīng)該是:在生成執(zhí)行上下文時,會有兩個階段掌桩。第一個階段是創(chuàng)建的階段(具體步驟是創(chuàng)建VO)边锁,JS 解釋器會找到需要提升的變量和函數(shù),并且給它們提前在內(nèi)存中開辟好空間波岛,函數(shù)的話會將整個函數(shù)存入內(nèi)存中茅坛,變量只聲明并且賦值為 undefined,所以在第二個階段,也就是代碼執(zhí)行階段贡蓖,我們可以直接提前使用曹鸠。
在提升的過程中,相同的函數(shù)會覆蓋上一個函數(shù)斥铺,并且函數(shù)優(yōu)先于變量提升
b() // call b second
function b() {
console.log('call b first')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
var 會產(chǎn)生很多錯誤彻桃,所以在ES6中引入了 let。let 不能在聲明前使用晾蜘,但是這并不是常說的 let 不會提示邻眷,let 提升了聲明但沒有賦值,因?yàn)榕R時死區(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ù)時肆饶,會創(chuàng)建一個輔助的特定對象,然后將函數(shù)名稱作為這個對象的屬性岖常,因此函數(shù)內(nèi)部才可以訪問到 foo驯镊,但是這個值又是只讀的,所以對它的賦值并不生效竭鞍,所以打印的結(jié)果還是這個函數(shù)阿宅,并且外部的值也沒有發(fā)生更改。
specialObject = {};
Scope = specialObject + Scope;
foo = new FuncionExpression;
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
}
經(jīng)典面試題滨砍,循環(huán)中使用閉包解決 var 定義函數(shù)的問題
for (var i = 0; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
首先因?yàn)?setTimeout 是個異步函數(shù)往湿,所以會先把循環(huán)全部執(zhí)行完畢,這時候 i 就是 6 了惋戏,所以會輸出一堆 6领追。
解決辦法兩種,第一種使用閉包
for (var i = 0; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
第二種就是使用 setTimeout 的第三個參數(shù)
for (var i = 0; i <= 5; i++) {
setTimeout(function timer(j) {
console.log(j)
}, i * 1000, i)
}
第三種就是使用 let 定義 i 了
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
因?yàn)閷τ?let 來說响逢,它會創(chuàng)建一個塊級作用域绒窑,相當(dāng)于
{ // 形成塊級作用域
let i = 0
{
let i1 = i
setTimeout(function timer() {
console.log(i1)
}, i*1000)
}
i++
{
let i1 = i
}
i++
{
let i1 = i
}
...
}
深淺拷貝
let a = {
age: 1
}
let b = a
a.age = 2
console.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 = 2
console.log(b.age) // 1
當(dāng)然我們也可以通過展開運(yùn)算符(...)來解決
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1
淺拷貝只解決了第一層的問題矛洞,如果接下去的值中還有對象的話洼哎,那么就又回到剛開始的話題了,兩者享有相同的引用。要解決這個問題噩峦,我們需要引入深拷貝锭沟。
深拷貝
這個問題通常可以通過 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
- 會忽略 symbol
- 不能序列化函數(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.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
如果有這么一個循環(huán)引用對象族淮,你會發(fā)現(xiàn)你不能通過該方法深拷貝
在遇到函數(shù)、undefined 或者 symbol 的時候李请,該對象也不能正常的序列化
let a = {
age: undefined,
sex: Symbol('male'),
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)引用對象
(async () => {
const clone = await structuralClone(obj)
})()
模塊化
在有 Babel 的情況下,我們可以直接使用 ES6 的模塊化
// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}
import {a, b} from './a.js'
import XXX from './b.js'
CommonJS
CommonJs 是 Node 獨(dú)有的規(guī)范底循,瀏覽器中使用就需要用到 Browserify 解析了巢株。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var 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.exports
var 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)出時都是支拷貝,就算導(dǎo)出的值變了喉誊,導(dǎo)入的值也不會改變邀摆,所以如果想更新值,必須重新導(dǎo)入一次伍茄。但是后者采用實(shí)時綁定的方式栋盹,導(dǎo)入導(dǎo)出的值都指向同一個內(nèi)存地址,所以導(dǎo)入值會隨導(dǎo)出值變化
- 后者會編譯成 require/exports 來執(zhí)行
AMD
AMD 是由 RequireJS 提出的
define(['./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()
})
防抖
防抖和節(jié)流的作用都是防止函數(shù)多次調(diào)用敷矫。區(qū)別在于例获,假設(shè)一個用戶一直觸發(fā)這個函數(shù),且每次觸發(fā)函數(shù)的間隔小于 wait曹仗,防抖的情況下只會調(diào)用一次榨汤,而節(jié)流的情況會每隔一定的時間(參數(shù) wait)調(diào)用函數(shù)。
我們先來看一個袖珍版的防抖理解以下防抖的實(shí)現(xiàn):
// func 是用戶傳入需要防抖的函數(shù)
// wait 是等待時間
const debounce = (func, wait = 50) => {
// 緩存一個定時器 id
let timer = 0
// 這里返回的函數(shù)是每次用戶實(shí)際調(diào)用的防抖函數(shù)
// 如果已經(jīng)設(shè)定過定時器了就清空上一次的定時器
// 開始一個新的定時器怎茫,延遲執(zhí)行用戶傳入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不難看出如果用戶調(diào)用該函數(shù)的間隔小于 wait 的情況下收壕,上一次的時間還未到就被清除了,并不會執(zhí)行函數(shù)
這是一個簡單版的防抖轨蛤,但是有缺陷蜜宪,這個防抖只能在最后調(diào)用。一般的防抖會有 immediate 選項(xiàng)祥山,表示是否立即調(diào)用圃验。這兩者的區(qū)別,舉個例子來說:
- 例如在搜索引擎問題的時候缝呕,我們當(dāng)然是希望用戶輸入完最后一個字才調(diào)用查詢接口损谦,這個時候適用延遲執(zhí)行的防抖函數(shù),它總是在一連串(間隔小于 wait的)函數(shù)觸發(fā)之后調(diào)用岳颇。
- 例如用戶給 interviewMap 點(diǎn) star 的時候照捡,我們希望用戶點(diǎn)第一下的時候就去調(diào)用接口,并且成功之后改變 star 按鈕的樣子话侧,用戶就可以立馬得到反饋是否 star 成功了栗精,這個情況適用立即執(zhí)行的防抖函數(shù),它總是在第一次調(diào)用瞻鹏,并且下次調(diào)用必須與前一次調(diào)用的時間間隔大于 wait 才會觸發(fā)悲立。
下面我們來實(shí)現(xiàn)一個帶有立即執(zhí)行選項(xiàng)的防抖函數(shù)
// 這里是用來獲取當(dāng)前時間戳的
function now() {
return +new Date()
}
/**
* 防抖函數(shù),返回函數(shù)連續(xù)調(diào)用時新博,空閑時間必須大于或等于 wait薪夕,func 才會執(zhí)行
*
* @param {function} func 回調(diào)函數(shù)
* @param {number} wait 表示時間窗口的間隔
* @param {boolean} immediate 設(shè)置為ture時,是否立即調(diào)用函數(shù)
* @return {function} 返回客戶調(diào)用函數(shù)
*/
function debounce(func, wait = 50, immediate = true) {
let timer, context, args
// 延遲執(zhí)行函數(shù)
const later = () => setTimeout(() => {
// 延遲執(zhí)行完畢赫悄,清空緩存的定時器序號
timer = null
// 延遲執(zhí)行的情況下原献,函數(shù)會在延遲函數(shù)種中執(zhí)行
// 使用到之前緩存的參數(shù)和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 這里返回的函數(shù)是每次實(shí)際調(diào)用的函數(shù)
return function(...params) {
// 如果沒有創(chuàng)建延遲執(zhí)行函數(shù)(later)馏慨,就會創(chuàng)建一個
if (!timer) {
timer = later()
// 如果是立即執(zhí)行,調(diào)用函數(shù)
// 否則緩存參數(shù)和調(diào)用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延遲執(zhí)行函數(shù)(later)姑隅,調(diào)用的時候清除原來的并重新設(shè)定一個
// 這樣做延遲函數(shù)會重新計(jì)時
} else {
clearTimeout(timer)
timer = later()
}
}
}
整體函數(shù)實(shí)現(xiàn)的不難写隶,總結(jié):
- 對于按鈕防點(diǎn)擊來說的實(shí)現(xiàn):如果函數(shù)是立即執(zhí)行的,就立即調(diào)用讲仰,如果函數(shù)是延遲執(zhí)行的慕趴,就緩存上下文和參數(shù),放在延遲函數(shù)中去執(zhí)行鄙陡。一旦開始一個定時器冕房,只要定時器還在,每次點(diǎn)擊都重新計(jì)時趁矾。一旦定時器時間到了耙册,定時器重置為 null,就可以再次點(diǎn)擊了愈魏。
- 對于延遲執(zhí)行函數(shù)來說的實(shí)現(xiàn):清除定時器 ID觅玻,如果是延遲調(diào)用就調(diào)用函數(shù)想际。
節(jié)流
防抖動和節(jié)流本質(zhì)是不一樣的培漏。防抖動是將多次執(zhí)行變?yōu)樽詈笠淮螆?zhí)行,節(jié)流是將多次執(zhí)行變成每隔一段時間執(zhí)行胡本。
/**
* underscore 節(jié)流函數(shù)牌柄,返回函數(shù)連續(xù)調(diào)用時,func 執(zhí)行頻率限定為 次 / wait
*
* @param {function} func 回調(diào)函數(shù)
* @param {number} wait 表示時間窗口的間隔
* @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;
// 之前的時間戳
var previous = 0;
// 如果 options 沒傳則設(shè)為空對象
if (!options) options = {};
// 定時器回調(diào)函數(shù)
var later = function() {
// 如果設(shè)置了 leading咒锻,就將 previous 設(shè)為 0
// 用于下面函數(shù)的第一個 if 判斷
previous = options.leading === false ? 0 : _.now();
// 置空一是為了防止內(nèi)存泄漏,二是為了下面的定時器判斷
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 獲取當(dāng)前時間戳
var now = _.now();
// 首次進(jìn)入前者肯定為 true
// 如果需要第一次不執(zhí)行函數(shù)
// 就將上次時間戳設(shè)為當(dāng)前的
// 這樣在接下來計(jì)算 remaining 的值時會大于 0
if (!previous && options.leading === false) previous = now;
// 計(jì)算剩余時間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果當(dāng)前調(diào)用已經(jīng)大于上次調(diào)用時間 + wait
// 或者用戶手動調(diào)了時間
// 如果設(shè)置了 trailing守屉,只會進(jìn)入這個條件
// 如果沒有設(shè)置 leading惑艇,那么第一次會進(jìn)入這個條件
// 還有一點(diǎn),你可能會覺得開啟了定時器那么應(yīng)該不會進(jìn)入這個 if 條件了
// 其實(shí)還是會進(jìn)入的拇泛,因?yàn)槎〞r器的延時
// 并不是準(zhǔn)確的時間滨巴,很可能你設(shè)置了2秒
// 但是他需要2.2秒才觸發(fā),這時候就會進(jìn)入這個條件
if (remaining <= 0 || remaining > wait) {
// 如果存在定時器就清理掉否則會調(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è)置了定時器和 trailing
// 沒有的話就開啟一個定時器
// 并且不能不能同時設(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 底層的這個限制。
call晨继,apply烟阐,bind 區(qū)別
首先說下前兩者的區(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'])
模擬實(shí)現(xiàn) call 和 apply
可以從一下幾點(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)
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 實(shí)現(xiàn)
Promise 是 ES6 新增的語法变隔,解決了回調(diào)地獄的問題。
可以把 Promise 看出一個狀態(tài)機(jī)蟹倾。初始是 pending 狀態(tài)匣缘,可以通過函數(shù) resolve 和 reject,將狀態(tài)轉(zhuǎn)變?yōu)?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í)例至多緩存一個
_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í)現(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.3
function 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 實(shí)現(xiàn)
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、flatMap 和 reduce
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
flatMap 和 map 的作用幾乎是相同的蝶涩,但是對于多維數(shù)組來說理朋,會將原數(shù)組降維絮识,可以將 flatMap 看成是 map + flatten,目前該函數(shù)在瀏覽器中還不支持嗽上。
[1, [2], 3].map((v) => v + 1)
// [2, 3, 4]
如果將想將一個多維數(shù)組徹底的降維次舌,可以這樣實(shí)現(xiàn)
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce((a, b) => [...a, ...flattenDeep(b)], [])
: [arr])
reduce 作用是數(shù)組中的值組合起來,最終得到一個值
function a(){
console.log(1)
}
function b(){
console.log(2)
}
[a, b].reduce((a, b) => a(b()))
// -> 2 1
async 和 await
一個函數(shù)如果加上 async兽愤,那么該函數(shù)就會返回一個 Promise
async function test(){
return '1'
}
console.log(test());
// 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)?await 會等待 sleep 函數(shù) resolve国拇,所以即使后面是同步代碼,也不會先去執(zhí)行同步代碼再來執(zhí)行異步代碼惯殊。
async 和 await 相比直接使用 Promise 來說酱吝,優(yōu)勢在于處理 then 的調(diào)用鏈,能夠更清晰準(zhǔn)確的寫出代碼土思。缺點(diǎn)在于濫用 await 可能會導(dǎo)致性能問題务热,因?yàn)?await 會阻塞代碼,也許之后的異步代碼并不依賴于前者己儒,但仍然需要等待前者完成崎岂,導(dǎo)致代碼失去了并發(fā)性。
下面來看一個使用 await 的代碼
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a)
a = (await 10) + a
console.log('3', a)
}
b()
a++
console.log('1', a)
- 首先函數(shù) b 先執(zhí)行闪湾,在執(zhí)行到 await 10 之前變量 a 還是 0冲甘,因?yàn)樵?await 內(nèi)部實(shí)現(xiàn)了 generators,generators 會保留堆棧中的東西途样,所以這時候 a = 0 被保存了下來江醇。
- 因?yàn)?await 是異步操作,遇到 await 就會立即返回一個 pending 狀態(tài)的 Promise 對象何暇,暫時返回執(zhí)行代碼的 控制權(quán)陶夜,使得函數(shù)外的代碼得以繼續(xù)執(zhí)行,所以會先執(zhí)行 console.log('1', a)裆站。
- 這時候同步代碼執(zhí)行完畢条辟,開始執(zhí)行異步代碼,將保存下來的值拿出來使用宏胯,這時候 a = 10羽嫡。
- 然后后面就是常規(guī)執(zhí)行代碼了。