JS核心知識梳理


前言

本文目標

從JS的運行設計數據應用四個角度來梳理JS核心的知識點

主題大綱

  1. JS運行

    • 變量提升
    • 執行上下文
    • 作用域
    • let
    • 作用域鏈
    • 閉包
    • 事件循環
  2. JS設計

    • 原型
    • 原型鏈
    • this
    • call
    • apply
    • bind
    • new
    • 繼承
  3. JS數據

    • 數據類型
    • 數據的存儲(深淺拷貝)
    • 數據類型判斷(隱式轉換,相等和全等,兩個對象相等)
    • 數據的操作(數組遍歷,對象遍歷)
    • 數據的計算(計算誤差)
  4. JS應用

    • 防抖,節流,柯里化

一. JS運行

大概分為四個階段

  1. 詞法分析:將js代碼中的字符串分割為有意義的代碼塊,稱為詞法單元

    • 瀏覽器剛拿到一個JS文件或者一個script代碼段的時候,它會認為里面是一個長長的字符串
    • 這是無法理解的,所以要分割成有意義的代碼塊,比如: var a = 1
  2. 語法分析:將詞法單元流轉換成一顆 抽象語法樹(AST),並對生成的AST樹節點進行處理,

    • 比如使用了ES6語法,用到了let,const,就要轉換成var。

為什么需要抽象語法樹呢?

  • 抽象語法樹是不依賴於具體的文法,不依賴於語言的細節,方便做很多的操作
  • 另一方面說,現在有許多語言,C,C++,Java,Javascript等等,他們有不同的語言規范
  • 但是轉化成抽象語法樹后就都是一致的了,方便編譯器對其進行進一步的增刪改查等操作,
  1. 預解析階段:

    • 會確定作用域規則
    • 變量和函數提升
  2. 執行階段:

    • 創建執行上下文,生成執行上下文棧
    • 執行可執行代碼,依據事件循環

1.作用域

指定了函數和變量的作用范圍

  • 分為全局作用域函數作用域
  • JS不像C,JAVA語言一樣,沒有塊級作用域,簡單說就是花括號的范圍

2.變量和函數提升

全局變量和函數聲明會提升

  • 函數聲明方式有三種,
    • function foo() {}
    • var foo = function () {}
    • var foo = new Function()
    • 可歸為兩類,直接創建變量賦值
  • 變量賦值函數賦值普通變量的優先級按位置來,變量名相同前者被覆蓋
  • 函數直接創建優先級高於變量賦值,同名取前者,與位置無關,也就是說函數直接創建即使再變量聲明后面,也是優先級最高

3. 執行上下文

有不同的作用域,就有不同的執行環境,我們需要來管理這些上下文的變量

  • 執行環境分為三種,執行上下文對應執行環境
    • 全局執行環境
    • 函數執行環境
    • eval執行環境(性能問題不提)
  1. 全局執行上下文
    • 先找變量聲明,
    • 再找函數聲明
  2. 函數執行上下文
    • 先找函數形參,和變量聲明
    • 把實參賦值給形參
    • 找函數聲明
  • 多個函數嵌套,就會有多個執行上下文,這需要執行上下文棧來維護,后進先出
  • 執行上下文里包含着變量環境詞法環境
  • 變量環境里就包含着當前環境里可使用的變量
  • 當前環境沒有用哪的, 這就說到了作用域鏈

4. 作用域鏈

  • 引用JS高程的定義:作用域鏈來保證對執行環境有權訪問的變量和函數的有序訪問
  • 變量的查找順序不是按執行上下文棧的順序,而是由詞法作用域決定的
  • 詞法作用域也就是靜態作用域,由函數聲明的位置決定,和函數在哪調用無關,也就js這么特殊

5. 靜態作用域和動態作用域

  • 詞法作用域是在寫代碼或者定義時確定的
  • 而動態作用域是在運行時確定的(this也是!
    var a = 2; function foo() { console.log(a); // 靜態2 動態3 } function bar() { var a = 3; foo(); } bar(); 復制代碼

閉包

  • 由於作用域的限制,我們無法在函數作用域外部訪問到函數內部定義的變量,而實際需求需要,這里就用到了閉包
  • 引用JS權威指南定義:閉包是指有權訪問另一個函數作用域中的變量的函數

1. 閉包作用

  • for循環遍歷進行事件綁定 輸出i值時為for循環的長度+1
  • 這結果顯示不是我們想要的, 因為JS沒有塊級作用域,var定義的i值,沒有銷毀,存儲與全局變量環境中
  • 在事件具體執行的時候取的i值,就是全局變量中經過多次計算后的i值
    for(var i = 0;i < 3;i++){ document.getElementById(`item${i+1}`).onclick = function() { console.log(i);//3,3,3 } } 復制代碼
  • 閉包特性:外部函數已經執行結束,內部函數引用外部函數的變量依然保存在內存中,變量的集合可稱為閉包
  • 在編譯過程中,對於內部函數,JS引擎會做一次此法掃描,如果引用了外部函數的變量,堆空間創建換一個Closure的對象,用來存儲閉包變量
  • 利用此特性給方法增加一層閉包存儲當時的i值, 將事件綁定在新增的匿名函數返回的函數上
for(var i = 0;i < 3;i++){ document.getElementById(`item${i+1}`).onclick = make(i); } function make(e) { return function() { console.log(e)//0,1,2 }; 復制代碼

閉包注意

  • 我們在不經意間就寫成了閉包,內部函數引用外部函數的變量依然保存在內存中,
  • 該銷毀的沒有銷毀,由於疏忽或錯誤造成程序未能釋放已經不再使用的內存,就造成了內存泄漏
  • 同時注意閉包不會造成內存泄漏,我們錯誤的使用閉包才是內存泄漏

事件循環

  • JS代碼執行依據 事件循環
  • JS是單線程,通過異步保證執行不被阻塞
  1. 執行機制
    • 簡單說就是,一個執行棧,兩個任務隊列
    • 發現宏任務就放入宏任務隊列,發現微任務就放入微任務隊列,
    • 執行棧為空時,執行微任務隊列所有微任務,再取宏任務隊列一個宏任務執行
    • 如此循環
  2. 宏&微任務 macroTask: setTimeout, setInterval, I/O, UI rendering microTask: Promise.then

二. JS設計

1. 原型

  1. JS的設計
  • 有new操作符,構造函數,卻沒有類的概念,而是使用原型來模擬類來實現繼承
  1. JS設計心路歷程
  • JS在設計之初,給的時間較短,並且定義為簡單的網頁腳本語言,不用太復雜,且想要模仿Java的理念,(這也是為什么JS叫JavaScript的原因)
  • 因此就借鑒了Java的對象構造函數new操作符理念,而拋棄掉了了復雜的class(類)概念
  1. 繼承機制
  • 需要有一種繼承的機制,來把所有對象聯系起來,就可以使用構造函數
  • 但是構造函數生成實例對象的缺點就是無法共享屬性和方法
  1. prototype屬性
  • 為解決上面問題,就引入了prototype屬性,就是我們常說的原型
  • 為構造函數設置一個prototype屬性,實例對象需要共享的方法,都放在此對象上,

整個核心設計完成后,后面的API也就順理成章

原型

  • 每一個js對象在創建的時候就會與之關聯另一個對象
  • 這個對象就是原型,每個對象都會從原型繼承屬性

proto

  • 每個對象都有一個屬性叫proto,該屬性指向對象的原型
  • 構造函數的prototype屬性等於實例化對象的proto屬性
  • 此屬性並不是ES5 中的規范屬性,只是為了在瀏覽器中方便獲取原型而做的一個語法糖,
  • 我們可以使用Object.getPrototype()方法獲取原型

constructor 原型沒有指向實例,因為一個構造函數可以有多個對象實例 但是原型指向構造函數是有的,每個原型都有一個constructor屬性指向關聯的構造函數

function Per() {} // 構造函數 const chi = new Per() // 實例對象 chi.__proto__ === Per.prototype // 獲取對象的原型 也是就構造函數的prototype屬性 Per.prototype.constructor === Per // constructor屬性 獲取當前原型關聯的構造函數 復制代碼

實例與原型

  • 讀取實例屬性找不到時,就會查找與對象關聯的原型的屬性,一直向上查找,
  • 這種實例與原型之間的鏈條關系,這就形成了原型鏈
    function Foo() {} Foo.prototype.name = 'tom' const foo = new Foo() foo.name = 'Jerry' console.log(foo.name); // Jerry delete foo.name console.log(foo.name); // tom 復制代碼

2.原型鏈

首先亮出大家熟悉的網圖

原型鏈關系圖

 

就是實例與構造函數,原型之間的鏈條關系

  • 實例的 proto 指向 原型

  • 構造函數的 prototype 屬性 指向 原型

  • 原型的 constructor 屬性 指向 構造函數

  • 所有構造函數的 proto 指向 Function.prototype

  • Function.prototype proto 指向 Object.prototype

  • Object.prototype proto 指向 null

函數對象原型(Function.prototype) 是負責造構造函數的機器,包含Object、String、Number、Boolean、Array,Function。 再由構造函數去制造具體的實例對象

function Foo() {} // 1. 所有構造函數的 __proto__ 指向 Function.prototype Foo.__proto__ // ƒ () { [native code] } Function.__proto__ // ƒ () { [native code] } Object.__proto__ // ƒ () { [native code] } // 2. 所有構造函數原型和new Object創造出的實例 __proto__ 指向 Object.prototype var o = new Object() o.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ ...} Function.prototype.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ ...} // 3. Object.prototype 指向null Object.prototype.__proto__ // null 復制代碼

2. this

  1. 作為對象方法調用時,指向該對象 obj.b(); // 指向obj`
  2. 作為函數方法調用, var b = obj.b; b(); // 指向全局window(函數方法其實就是window對象的方法,所以與上同理,誰調用就指向誰)
  3. 作為構造函數調用 var b = new Per(); // this指向當前實例對象
  4. 作為call與apply調用 obj.b.apply(object, []); // this指向當前指定的值

誰調用就指向誰


const obj = {a: 1, f: function() {console.log(this, this.a)}} obj.f(); // 1 const a = 2; const win = obj.f; win(); // 2 function Person() { this.a = 3; this.f = obj.f; } const per = new Person() per.f() // 3 const app = { a: 4 } obj.f.apply(app); // 4 復制代碼

this 指向是動態作用域,誰調用指向誰,

3. call

  1. 定義:使用一個指定的 this 值和若干個指定的參數值的前提下調用某個函數或方法
  2. 舉例:
var obj = { value: 1, } function foo (name, old) { return { value:this.value, name, old } } foo.call(obj, 'tom', 12); // {value: 1, name: "tom", old: 12} 復制代碼
  1. 要求:
  • call改變了this的指向,
  • 執行了foo函數
  • 支持傳參,參數個數不固定
  • this參數可以傳null,傳null時 指向window
  • 函數可以有返回值
  • 非函數調用判斷處理
  1. 實現及思路
Function.prototype.call1 = function (context, ...args) { if(typeof this !== 'function') {throw new Error('Error')} // 非函數調用判斷處理 context = context || window; // 非運算符判定傳入參數 null則指向window const key = Symbol(); // 利用symbol創建唯一的屬性名,防止覆蓋原有屬性 context[key] = this; // 把函數作為對象的屬性,函數的this就指向了對象 const result = context[key](...args) // 臨時變量賦值為執行對象方法 delete context[key]; // 刪除方法,防止污染對象 return result // return 臨時變量 } 復制代碼
  1. 應用場景 改變this指向,大部分是為了借用方法或屬性
    1. 判斷數據類型,借用ObjecttoString方法 Object.prorotype.toString.call()
    1. 子類繼承父類屬性,function Chi() {Par.call(this)}

4. apply

  1. 定義:使用一個指定的 this 值和一個數組(數組包含若干個指定的參數值)的前提下調用某個函數或方法
  2. 舉例:
var obj = { value: 1, } function foo (name, old) { return { value:this.value, name, old } } foo.apply(obj, ['tom', 12], 24); // {value: 1, name: "tom", old: 12} 復制代碼
  1. 要求:
  • call改變了this的指向,
  • 執行了foo函數
  • 支持傳參,第二個參數為數組,之后的參數無效,數組內參數個數不固定,
  • this參數可以傳null,傳null時 指向window
  • 函數可以有返回值
  • 非函數調用判斷處理
  1. 實現及思路
Function.prototype.apply1 = function (context, args) { // 與call實現的唯一區別,此處不用解構 if(typeof this !== 'function') {throw new Error('Error')} // 非函數調用判斷處理 context = context || window; // 非運算符判定傳入參數 null則指向window const key = Symbol(); // 利用symbol創建唯一的屬性名,防止覆蓋原有屬性 context[key] = this; // 把函數作為對象的屬性,函數的this就指向了對象 const result = context[key](...args) // 臨時變量賦值為執行對象方法 delete context[key]; // 刪除方法,防止污染對象 return result // return 臨時變量 } 復制代碼
  1. 應用場景
  • 同 call

5. bind

  1. 定義
    • bind方法創建一個新函數,
    • 新函數被調用時,第一個參數為運行時的this,
    • 之后的參數會在傳遞實參前傳入作為它的參數
  2. 舉例
const obj = { value: 1 }; function foo(name, old) { return { value: this.value, name, old } } const bindFoo = foo.bind(obj, 'tom'); bindFoo(12); // {value: 1, name: "tom", old: 12} 復制代碼
  1. 要求
    • 返回一個函數,第一個參數作為執行時this
    • 可以在bind時傳一部分參,執行函數時再傳一部分參
    • bind函數作為構造函數,this失效,但參數有效,
    • 並且作為構造函數時,原型應指向綁定函數的原型,以便實例來繼承原型中的值
    • 非函數調用bind判斷處理
  2. 實現及思路
Function.prototype.bind1 = function (context, ...args) { if(typeof this !== 'function') {throw new Error('Error')} // 非函數調用判斷處理 const self = this; // 保存當前執行環境的this const Foo = function() {} // 保存原函數原型 const res = function (...args2) { // 創建一個函數並返回 return self.call( // 函數內返回 this instanceof res ? this : context, // 作為構造函數時,this指向實例,:作為普通函數this正常指向傳入的context ...args, ...args2) // 兩次傳入的參數都作為參數返回 } Foo.prototype = this.prototype; // 利用空函數中轉,保證在新函數原型改變時bind函數原型不被污染 res.prorotype = new Foo(); return res; } 復制代碼

6. new

看看new出的實例能做什么

  • 可訪問構造函數的屬性
  • 訪問prototype的屬性
  • 構造函數如有返回值,返回對象,實例則只能訪問返回的對象中的屬性,this無效
  • 返回基本類型值,正常處理,this有效
function Persion(name, age) { this.name = name; this.age = age; } Person.prototype.sayName = function () { console.log(this.name) } var per = new Person('tom', 10) per.name // 10 可訪問構造函數的屬性 per.sayName // tom 訪問prototype的屬性 復制代碼

new實現

var person = factory(Foo) function factory() { // new是關鍵字,無法覆蓋,函數替代 var obj = {}; // 新建對象obj var con = [].shift.call(arguments); // 取出構造函數 obj._proto_ = con.prototype; // obj原型指向構造函數原型 var res = con.apply(obj, arguments); // 構造函數this指向obj return typeof(res) === 'object' ? ret : obj; } 復制代碼

7. 繼承

  1. 原型鏈繼承和原型式繼承
  • 無法向父類傳參數,只能共用屬性
  1. 借用構造函數和寄生繼承
  • 方法不在原型上,每次都要重新創建
  1. 組合繼承
  • 雖然解決了以上倆個問題,但是調用了兩次父親,
  • 實例和原型上會用相同屬性
  1. 寄生組合繼承
  • 目前最優方案

寄生組合繼承實現

function P (name) { this.name = name; } // 父類上綁定屬性動態傳參 P.prototype.sayName = function() { // 父類原型上綁定方法 console.log(111) } function F(name) { // 子類函數里,父類函數利用`call`函數this指向子類,傳參並執行 P.call(this, name) } const p = Object.create(P.prototype) // Object.create 不會繼承構造函數多余的屬性和方法 p.constructor = F; // constructor屬性丟失,重新指向 F.prototype = p; // 子類原型 指向 中轉對象 const c = new F('tom'); // 子類實例 化 c.name // tom c.sayName() // 111 復制代碼

三. JS數據

1.數據類型

JS分為基本類型和引用類型

  • 基本類型:Boolean Undefined String Number Null Symbol
  • 引用類型:Object Funciton Array 等

2.數據存儲 (深淺拷貝)

js數據類型中分為基本類型,和引用類型

  1. 基本類型 保存在棧內存中
  • 賦值時 編譯系統重新創建一塊內存來存儲新的變量 所以基本類型變量賦值后就斷絕了關系
  1. 引用類型 保存在堆內存中
  • 賦值時 只是對對象地址的拷貝 沒有開辟新的內存,堆內存地址的拷貝,兩者指向了同一地址
  • 修改其中一個,另外一個就會收到影響

兩個對象 指向了同一地址 修改其中一個就會影響另一個

特殊的數組對象方法(深拷貝一層)

  1. obj2 = Object.assign({}, obj1)

  2. arr2 = [].concat(arr1)

  3. arr2 = arr1.slice(0)

  4. arr2 = Array.form(arr1)

  5. arr2 = [...arr1];

以上方法都只能深拷貝一層

JSON.parse(JSON.stringify(obj))(多層) 不拷貝一個對象,而是拷貝一個字符串,會開辟一個新的內存地址,切斷了引用對象的指針聯系

缺點

  1. 時間對象 => 字符串的形式
  2. RegExp、Error => 只得到空對象
  3. function,undefined => 丟失
  4. NaN、Infinity和-Infinity => 序列化成null
  5. 對象是由構造函數生成 => 會丟棄對象的 constructor
  6. 存在循環引用的情況也無法實現深拷貝

手動實現深拷貝(多層)

    function Judgetype(e) { return Object.prototype.toString.call(e).slice(8, -1).toLowerCase(); } function Loop(param) { let target = null; if(Judgetype(param) === 'array') { target = []; for(let key of param.keys()){ target[key] = Deep(param[key]); } } else { target = {}; Object.keys(obj).forEach((val) => { target[key] = Deep(param[key]); }) } return target; } function Deep(param) { //基本數據類型 if(param === null || (typeof param !== 'object' && typeof param !== 'function')) { return param; } //函數 if(typeof param === 'function') { return new Function('return ' + param.toString())(); } return Loop(param); } 復制代碼

3.數據類型判斷(類型判斷,相等和全等,隱式轉換,兩個對象相等)

  1. 類型判斷 typeof
  • 無法區分 object, null 和 array
  • 對於基本類型,除 null 以外,均可以返回正確的結果。
  • 對於引用類型,除 function 以外,一律返回 object 類型
typeof(1) // number typeof('tom') // string typeof(undefined) // undefined typeof(null) // object typeof(true) // boolean typeof(Symbol(1)) // symbol typeof({a: 1}) // object typeof(function () {}) // function typeof([]) // object 復制代碼

instanceof

  • 判斷一個實例是否屬於某種類型
[] instanceof Array; // true {} instanceof Object;// true var a = function (){} a instanceof Function // true 復制代碼

Object.prototype.toString.call

  • 目前最優方案

  • toString() 是 Object 的原型方法,調用該方法,默認返回當前對象的 [[Class]] 。這是一個內部屬性,其格式為 [object Xxx] ,其中 Xxx 就是對象的類型。

  • 對於 Object 對象,直接調用 toString() 就能返回 [object Object] 。而對於其他對象,則需要通過 call / apply 來調用才能返回正確的類型信息

  • 這里就是call的應用場景,巧妙利用call借用Object的方法實現類型的判斷

function T(e) { return Object.prototype.toString.call(e).slice(8, -1).toLowerCase(); } T(1) // number T('a') // string T(null) // null T(undefined) // undefined T(true) // boolean T(Symbol(1)) // symbol T({a: 1}) // object T(function() {}) // function T([]) // array T(new Date()) // date T(/at/) // RegExp 復制代碼
  1. 相等和全等
  • == 非嚴格比較 允許 類型轉換
  • === 嚴格比較 不允許 類型轉換

引用數據類型棧中存放地址,堆中存放內容,即使內容相同 ,但地址不同,所以兩者還是不等的

const a = {c: 1} const b = {c: 1} a === b // false 復制代碼
  1. 隱式轉換
  • == 可能會有類型轉換,不僅在相等比較上,在做運算的時候也會產生類型轉換,這就是我們說的隱式轉換

布爾比較,先轉數字

true == 2 // false || 1 == 2 // if(X) var X = 10 if(X) // true 10 ==> true // if(X == true) if(X == true) // false 10 == true || 10 == 1 復制代碼

數字和字符串做比較,字符串數字

0 == '' // true || 0 == 0 1 == '1' // true || 1 == 1 復制代碼

對象類型和原始類型的相等比較


[2] == 2 // true || valueOf() // 調用valueOf() 取自身值 [2] == 2 || toString() // 調用toString() 轉字符串 "2" == 2 || Number() // // 數字和字符串做比較,`字符串`轉`數字` 2 == 2 復制代碼

小結 js使用某些操作符會導致類型變換, 常見是+,==

  1. 運算時
  • 加法 存在一個字符串都轉字符串
  • 乘 - 除 - 減法 字符串都轉數字
  1. 相等比較時
  • 布爾比較,先轉數字
  • 數字和字符串做比較,字符串數字
  • 對象類型比較先轉原始類型

課外小題 實現 a == 1 && a == 2 && a == 3

const a = { i: 1, toString: function () { return a.i++; } } a == 1 && a == 2 && a == 3 復制代碼
  1. 判斷兩個對象值相等
  • 引用數據類型棧中存放地址,堆中存放內容,即使內容相同 ,但地址不同,所以===判斷時兩者還是不等的
  • 但引用數據內容相同時我們如何判斷他們相等呢?
// 判斷兩者非對象返回 // 判斷長度是否一致 // 判斷key值是否相同 // 判斷相應的key值里的對應的值是否相同 這里僅僅考慮 對象的值為object,array,number,undefined,null,string,boolean 關於一些特殊類型 `function date RegExp` 暫不考慮 function Judgetype(e) { return Object.prototype.toString.call(e).slice(8, -1).toLowerCase(); } function Diff(s1, s2) { const j1 = Judgetype(s1); const j2 = Judgetype(s2); if(j1 !== j2){ return false; } if(j1 === 'object') { if(Object.keys(s1).length !== Object.keys(s2).length){ return false; } s1[Symbol.iterator] = function* (){ let keys = Object.keys( this ) for(let i = 0, l = keys.length; i < l; i++){ yield { key: keys[i], value: this[keys[i]] }; } } for(let {key, value} of s1){ if(!Diff(s1[key], s2[key])) { return false } } return true } else if(j1 === 'array') { if(s1.length !== s2.length) { return false } for(let key of s1.keys()){ if(!Diff(s1[key], s2[key])) { return false } } return true } else return s1 === s2 } Diff( {a: 1, b: 2}, {a: 1, b: 3}) // false Diff( {a: 1, b: [1,2]}, {a: 1, b: [1,3]}) // false 復制代碼

其實對象遍歷 return 也可以用for in, 關於遍歷原型的副作用可以用hasOwnproperty判斷去彌補

  • 對象for of 遍歷還得自己加迭代器,比較麻煩
    for(var key in s1) { if(!s1.hasOwnProperty(key)) { if(!Diff(s1[key], s2[key])) { return false } } } 復制代碼

4.數據操作(數組遍歷,對象遍歷)

1.數組遍歷

`最普通 for循環` // 較為麻煩 for(let i = 0,len = arr.length; i < len; i++) { console.log(i, arr[i]); } `forEach` 無法 break return `for in` 不適合遍歷數組, `for...in` 語句在w3c定義用於遍歷數組或者對象的屬性 1. index索引為字符串型數字,不能直接進行幾何運算 2. 遍歷順序有可能不是按照實際數組的內部順序 3. 使用for in會遍歷數組所有的可枚舉屬性,包括原型 `for of` 無法獲取下標 // for of 兼容1 for(let [index,elem] of new Map( arr.map( ( item, i ) => [ i, item ] ) )){   console.log(index);   console.log(elem); } // for of 兼容2 let arr = [1,2,3,4,5]; for(let key of arr.keys()){ console.log(key, arr[key]); } for(let val of arr.values()){ console.log(val); } for(let [key, val] of arr.entries()){ console.log(key, val); } 2. 復制代碼

2.對象遍歷

  1. for in

缺點:會遍歷出對象的所有可枚舉的屬性, 比如prototype上的

var obj = {a:1, b: 2} obj.__proto__.c = 3; Object.prototype.d = 4 for(let val in obj) { console.log(val) // a,b,c,d } // 優化 for(let val in obj) { if(obj.hasOwnProperty(val)) { // 判斷屬性是存在於當前對象實例本身,而非原型上 console.log(val) // a,b } } 復制代碼
  1. object.keys
var obj = { a:1, b: 2 } Object.keys(obj).forEach((val) => { console.log(val, obj[val]); // a 1 // b 2 }) 復制代碼
  1. for of
  • 只有提供了 Iterator 接口的數據類型才可以使用 for-of
  • Array 等類型是默認提供了的
  • 我們可以給 對象 加一個 Symbol.iterator 屬性
var obj = { a:1, b: 2 } obj[Symbol.iterator] = function* (){ let keys = Object.keys( this ) for(let i = 0, l = keys.length; i < l; i++){ yield { key: keys[i], value: this[keys[i]] }; } } for(let {key, value} of obj){ console.log( key, value ); // a 1 // b 2 } 復制代碼

5.數據計算(計算誤差)

  1. 0.1 + 0.2 = 0.30000000000000004
  • 所有的數都會轉換成二進制,逐位去計算,
  • 小數 二進制不能二等分的 會無限循環
  • js數據存儲 64 位雙精度浮點數,這里不做贅述,超出會被截取(大數計算誤差與限制也是因為這個)
  • 相加后再轉換回來就會出現誤差
  1. 那么如何做出精確的計算呢
  • 對於數字十進制本身不超過js存儲位數的小數,可以同時變為整數,計算后再化為小數
function getLen(n) { const str = n + ''; const s1 = str.indexOf('.') if(s1) { return str.length - s1 - 1 } else { return 0 } } function add(n1, n2) { const s1 = getLen(n1) const s2 = getLen(n2) const max = Math.max(s1, s2) return (n1 * Math.pow(10, max) + n2 * Math.pow(10, max)) / Math.pow(10, max) } add(11.2, 2.11) // 13.31 復制代碼
  • 對於超出存儲位數的可以,轉換成數組,倒序逐位相加,大於10進位,字符串拼接得到值
function add(a, b) { let i = a.length - 1; let j = b.length - 1; let carry = 0; let ret = ''; while(i>=0|| j>=0) { let x = 0; let y = 0; let sum; if(i >= 0) { x = a[i] - '0'; i-- } if(j >=0) { y = b[j] - '0'; j--; } sum = x + y + carry; if(sum >= 10) { carry = 1; sum -= 10; } else { carry = 0 } ret = sum + ret; } if(carry) { ret = carry + ret; } return ret; } add('999999999999999999999999999999999999999999999999999999999999999', '1') // 1000000000000000000000000000000000000000000000000000000000000000 復制代碼

四. JS應用

1.防抖

場景:

  • 搜索框輸入下拉聯想,請求后台接口,為了避免頻繁請求,給服務器造成壓力 定義:
  • 在事件觸發n秒后執行,在一個事件觸發n秒內又觸發了該事件,就以新的事件為准

實現思想:

  1. 定時器的執行與清除
  2. apply 改變this指向
  3. apply傳參繼承
function debounce(func, wait) { var timeout; return function () { var context = this; var args = arguments; clearTimeout(timeout) timeout = setTimeout(function(){ func.apply(context, args) }, wait); } } 復制代碼

2.節流

場景:

  • 可以將一些事件降低觸發頻率。
  • 比如懶加載時要監聽計算滾動條的位置,但不必每次滑動都觸發,可以降低計算的頻率,而不必去浪費資源;

定義:

  • 持續觸發事件,規定時間內,只執行一次
  1. 方法一:時間戳
  • 實現思想:
    • 觸發時間取當前時間戳now, 減去flag時間戳(初始值為0)
    • 如果大於規定時間,則執行,且flag更新為當前時間,
    • 如果小於規定時間,則不執行
function foo(func, wait) { var context, args; var flag = 0; return function () { var now = +new Date(); context = this; args = arguments; if(now - flag > 0) { func.apply(context, args); flag = now; } } } 復制代碼
  1. 方法二:定時器
  • 實現思想:
    • 判斷當前是否有定時器,
    • 沒有 就定義定時器,到規定時間執行,且清空定時器
    • 有則不執行
function foo(func, wait) { var context, args; var timeout; return function() { if(!timeout){ setTimeout(()=>{ timeout = null; func.apply(context, args); }, wait) } } } 復制代碼

3.柯里化

  1. 定義:將能夠接收多個參數的函數轉化為接收單一參數的函數,並且返回接收余下參數且返回結果的新函數
  2. 特點:參數復用,提前返回,延遲執行
  3. 實現過程
  • 創建一個函數,利用 apply,給柯里化函數重新傳入合並后的參數
  • 利用reduce迭代數組所有項,構建一個最終返回值
function add(...args) { var fn = function(...args1) { return add.apply(this, [...args, ...args1]); } fn.toString = function() { return args.reduce(function(a, b) { return a + b; }) } return fn; } add(1)(2)(3).toString(); // 6 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM