六脈神劍之核心脈——JS核心知識梳理

前言

本文目標

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

主題大綱

  1. JS運行瀏覽器

    • 變量提高
    • 執行上下文
    • 做用域
    • let
    • 做用域鏈
    • 閉包
    • 事件循環
  2. JS設計服務器

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

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

    • 防抖,節流,柯里化

一. JS運行

大概分爲四個階段app

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

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

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

爲何須要抽象語法樹呢?oop

  • 抽象語法樹是不依賴於具體的文法,不依賴於語言的細節,方便作不少的操做
  • 另外一方面說,如今有許多語言,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,undefinednull,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

複製代碼
相關文章
相關標籤/搜索