溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

前端js總結(jié)

發(fā)布時(shí)間:2020-08-04 06:09:02 來源:網(wǎng)絡(luò) 閱讀:4539 作者:夢想代碼 欄目:web開發(fā)

內(nèi)置類型

JS 中分為七種內(nèi)置類型,七種內(nèi)置類型又分為兩大類型:基本類型和對象(Object)。

基本類型有六種: nullundefined,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

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

#類型轉(zhuǎn)換

#轉(zhuǎn)Boolean

除了 undefined, null, false, NaN, '', 0, -0,其他所有值都轉(zhuǎn)為 true,包括所有對象。

#對象轉(zhuǎn)基本類型

對象在轉(zhuǎn)換基本類型時(shí),首先會調(diào)用 valueOf 然后調(diào)用 toString。并且這兩個方法你是可以重寫的。

let a = {
    valueOf() {
    	return 0
    }}

#四則運(yùn)算符

只有當(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

#== 操作符

前端js總結(jié)


上圖中的 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

#比較運(yùn)算符

  1. 如果是對象,就通過 toPrimitive 轉(zhuǎn)換對象

  2. 如果是字符串,就通過 unicode 字符索引來比較

#原型

前端js總結(jié)


每個函數(shù)都有 prototype 屬性,除了 Function.prototype.bind(),該屬性指向原型。

每個對象都有 __proto__ 屬性,指向了創(chuàng)建該對象的構(gòu)造函數(shù)的原型。其實(shí)這個屬性指向了 [[prototype]],但是 [[prototype]] 是內(nèi)部屬性,我們并不能訪問到,所以使用 _proto_ 來訪問。

對象可以通過 __proto__ 來尋找不屬于該對象的屬性,__proto__ 將對象連接起來組成了原型鏈。

如果你想更進(jìn)一步的了解原型,可以仔細(xì)閱讀 深度解析原型中的各個難點(diǎn)。

#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}

對于實(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

前端js總結(jié)


從上圖可以看出,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

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 = 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 一旦綁定了上下文,就不會被任何代碼改變。

#執(zhí)行上下文

當(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)你不能通過該方法深拷貝

前端js總結(jié)

在遇到函數(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

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

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é)流

防抖動和節(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)錯

前端js總結(jié)


因?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)
  // 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 實(shí)現(xiàn)

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 的完整測試

前端js總結(jié)

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、FlapMap 和 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

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

#async 和 await

一個函數(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

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

#為什么 0.1 + 0.2 != 0.3

因?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)制的呢,我們可以來演算下

前端js總結(jié)

小數(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))

#正則表達(dá)式

#元字符

元字符作用
.匹配任意字符除了換行符
[]匹配方括號內(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和上面相反


向AI問一下細(xì)節(jié)

免責(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)容。

AI