前言
本文目標
從JS的運行
,設計
,數據
,應用
四個角度來梳理JS核心的知識點
主題大綱
-
JS運行
- 變量提升
- 執行上下文
- 作用域
- let
- 作用域鏈
- 閉包
- 事件循環
-
JS設計
- 原型
- 原型鏈
- this
- call
- apply
- bind
- new
- 繼承
-
JS數據
- 數據類型
- 數據的存儲(深淺拷貝)
- 數據類型判斷(隱式轉換,相等和全等,兩個對象相等)
- 數據的操作(數組遍歷,對象遍歷)
- 數據的計算(計算誤差)
-
JS應用
- 防抖,節流,柯里化
一. JS運行
大概分為四個階段
-
詞法分析:將js代碼中的字符串分割為有意義的代碼塊,稱為
詞法單元
- 瀏覽器剛拿到一個JS文件或者一個script代碼段的時候,它會認為里面是一個長長的字符串
- 這是無法理解的,所以要分割成有意義的代碼塊,比如:
var a = 1
-
語法分析:將
詞法單元
流轉換成一顆抽象語法樹(AST)
,並對生成的AST樹節點進行處理,- 比如使用了ES6語法,用到了let,const,就要轉換成var。
為什么需要抽象語法樹呢?
- 抽象語法樹是不依賴於具體的文法,不依賴於語言的細節,方便做很多的操作
- 另一方面說,現在有許多語言,C,C++,Java,Javascript等等,他們有不同的語言規范
- 但是轉化成抽象語法樹后就都是一致的了,方便編譯器對其進行進一步的增刪改查等操作,
-
預解析階段:
- 會確定作用域規則
- 變量和函數提升
-
執行階段:
- 創建執行上下文,生成執行上下文棧
- 執行可執行代碼,依據事件循環
1.作用域
指定了函數和變量的作用范圍
- 分為
全局作用域
和函數作用域
, - JS不像C,JAVA語言一樣,沒有塊級作用域,簡單說就是花括號的范圍
2.變量和函數提升
全局變量和函數聲明會提升
- 函數聲明方式有三種,
function foo() {}
var foo = function () {}
var foo = new Function()
- 可歸為兩類,
直接創建
和變量賦值
變量賦值函數
和賦值普通變量
的優先級按位置來,變量名相同前者被覆蓋- 函數直接創建優先級高於變量賦值,同名取前者,與位置無關,也就是說函數直接創建即使再變量聲明后面,也是優先級最高
3. 執行上下文
有不同的作用域,就有不同的執行環境,我們需要來管理這些上下文的變量
- 執行環境分為三種,執行上下文對應執行環境
- 全局執行環境
- 函數執行環境
- eval執行環境(性能問題不提)
- 全局執行上下文
- 先找變量聲明,
- 再找函數聲明
- 函數執行上下文
- 先找函數形參,和變量聲明
- 把實參賦值給形參
- 找函數聲明
- 多個函數嵌套,就會有多個執行上下文,這需要
執行上下文棧
來維護,后進先出 - 執行上下文里包含着
變量環境
和詞法環境
變量環境
里就包含着當前環境里可使用的變量- 當前環境沒有用哪的, 這就說到了
作用域鏈
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是單線程,通過異步保證執行不被阻塞
- 執行機制
- 簡單說就是,一個執行棧,兩個任務隊列
- 發現宏任務就放入宏任務隊列,發現微任務就放入微任務隊列,
- 執行棧為空時,執行微任務隊列所有微任務,再取宏任務隊列一個宏任務執行
- 如此循環
- 宏&微任務 macroTask: setTimeout, setInterval, I/O, UI rendering microTask: Promise.then
二. JS設計
1. 原型
- JS的設計
- 有new操作符,構造函數,卻沒有類的概念,而是使用原型來模擬類來實現繼承
- JS設計心路歷程
- JS在設計之初,給的時間較短,並且定義為簡單的網頁腳本語言,不用太復雜,且想要模仿Java的理念,(這也是為什么JS叫JavaScript的原因)
- 因此就借鑒了Java的
對象
、構造函數
、new
操作符理念,而拋棄掉了了復雜的class
(類)概念
- 繼承機制
- 需要有一種
繼承
的機制,來把所有對象聯系起來,就可以使用構造函數 - 但是構造函數生成實例對象的缺點就是
無法共享屬性和方法
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
- 作為對象方法調用時,指向該對象
obj.b();
// 指向obj` - 作為函數方法調用,
var b = obj.b; b();
// 指向全局window(函數方法其實就是window對象的方法,所以與上同理,誰調用就指向誰) - 作為構造函數調用
var b = new Per();
// this指向當前實例對象 - 作為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
- 定義:使用一個指定的 this 值和若干個指定的參數值的前提下調用某個函數或方法
- 舉例:
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} 復制代碼
- 要求:
- call改變了this的指向,
- 執行了foo函數
- 支持傳參,參數個數不固定
- this參數可以傳null,傳null時 指向window
- 函數可以有返回值
- 非函數調用判斷處理
- 實現及思路
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 臨時變量 } 復制代碼
- 應用場景 改變this指向,大部分是為了借用方法或屬性
-
- 判斷數據類型,借用
Object
的toString
方法Object.prorotype.toString.call()
- 判斷數據類型,借用
-
- 子類繼承父類屬性,
function Chi() {Par.call(this)}
- 子類繼承父類屬性,
4. apply
- 定義:使用一個指定的 this 值和一個數組(數組包含若干個指定的參數值)的前提下調用某個函數或方法
- 舉例:
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} 復制代碼
- 要求:
- call改變了this的指向,
- 執行了foo函數
- 支持傳參,第二個參數為數組,之后的參數無效,數組內參數個數不固定,
- this參數可以傳null,傳null時 指向window
- 函數可以有返回值
- 非函數調用判斷處理
- 實現及思路
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 臨時變量 } 復制代碼
- 應用場景
- 同 call
5. bind
- 定義
- bind方法創建一個新函數,
- 新函數被調用時,第一個參數為運行時的this,
- 之后的參數會在傳遞實參前傳入作為它的參數
- 舉例
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} 復制代碼
- 要求
- 返回一個函數,第一個參數作為執行時this
- 可以在bind時傳一部分參,執行函數時再傳一部分參
- bind函數作為構造函數,this失效,但參數有效,
- 並且作為構造函數時,原型應指向綁定函數的原型,以便實例來繼承原型中的值
- 非函數調用bind判斷處理
- 實現及思路
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. 繼承
- 原型鏈繼承和原型式繼承
- 無法向父類傳參數,只能共用屬性
- 借用構造函數和寄生繼承
- 方法不在原型上,每次都要重新創建
- 組合繼承
- 雖然解決了以上倆個問題,但是調用了兩次父親,
- 實例和原型上會用相同屬性
- 寄生組合繼承
- 目前最優方案
寄生組合繼承實現
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數據類型中分為基本類型,和引用類型
- 基本類型 保存在棧內存中
- 賦值時 編譯系統重新創建一塊內存來存儲新的變量 所以基本類型變量賦值后就斷絕了關系
- 引用類型 保存在堆內存中
- 賦值時 只是對對象地址的拷貝 沒有開辟新的內存,堆內存地址的拷貝,兩者指向了同一地址
- 修改其中一個,另外一個就會收到影響
兩個對象 指向了同一地址 修改其中一個就會影響另一個
特殊的數組對象方法(深拷貝一層)
-
obj2 = Object.assign({}, obj1)
-
arr2 = [].concat(arr1)
-
arr2 = arr1.slice(0)
-
arr2 = Array.form(arr1)
-
arr2 = [...arr1];
以上方法都只能深拷貝一層
JSON.parse(JSON.stringify(obj))(多層) 不拷貝一個對象,而是拷貝一個字符串,會開辟一個新的內存地址,切斷了引用對象的指針聯系
缺點
- 時間對象 => 字符串的形式
- RegExp、Error => 只得到空對象
- function,undefined => 丟失
- NaN、Infinity和-Infinity => 序列化成null
- 對象是由構造函數生成 => 會丟棄對象的 constructor
- 存在循環引用的情況也無法實現深拷貝
手動實現深拷貝(多層)
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.數據類型判斷(類型判斷,相等和全等,隱式轉換,兩個對象相等)
- 類型判斷 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 復制代碼
- 相等和全等
==
非嚴格比較 允許 類型轉換===
嚴格比較 不允許 類型轉換
引用數據類型棧中存放地址,堆中存放內容,即使內容相同 ,但地址不同,所以兩者還是不等的
const a = {c: 1} const b = {c: 1} a === b // false 復制代碼
- 隱式轉換
==
可能會有類型轉換,不僅在相等比較上,在做運算的時候也會產生類型轉換,這就是我們說的隱式轉換
布爾比較,先轉數字
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使用某些操作符會導致類型變換, 常見是+,==
- 運算時
- 加法 存在一個字符串都轉字符串
- 乘 - 除 - 減法 字符串都轉數字
- 相等比較時
- 布爾比較,先轉數字
- 數字和字符串做比較,
字符串
轉數字
- 對象類型比較先轉原始類型
課外小題 實現 a == 1 && a == 2 && a == 3
const a = { i: 1, toString: function () { return a.i++; } } a == 1 && a == 2 && a == 3 復制代碼
- 判斷兩個對象值相等
- 引用數據類型棧中存放地址,堆中存放內容,即使內容相同 ,但地址不同,所以
===
判斷時兩者還是不等的 - 但引用數據內容相同時我們如何判斷他們相等呢?
// 判斷兩者非對象返回 // 判斷長度是否一致 // 判斷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.對象遍歷
- 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 } } 復制代碼
- object.keys
var obj = { a:1, b: 2 } Object.keys(obj).forEach((val) => { console.log(val, obj[val]); // a 1 // b 2 }) 復制代碼
- 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.數據計算(計算誤差)
0.1 + 0.2 = 0.30000000000000004
- 所有的數都會轉換成二進制,逐位去計算,
- 小數 二進制不能二等分的 會無限循環
- js數據存儲 64 位雙精度浮點數,這里不做贅述,超出會被截取(大數計算誤差與限制也是因為這個)
- 相加后再轉換回來就會出現誤差
- 那么如何做出精確的計算呢
- 對於數字十進制本身不超過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秒內又觸發了該事件,就以新的事件為准
實現思想:
- 定時器的執行與清除
- apply 改變this指向
- 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.節流
場景:
- 可以將一些事件降低觸發頻率。
- 比如懶加載時要監聽計算滾動條的位置,但不必每次滑動都觸發,可以降低計算的頻率,而不必去浪費資源;
定義:
- 持續觸發事件,規定時間內,只執行一次
- 方法一:時間戳
- 實現思想:
- 觸發時間取當前時間戳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; } } } 復制代碼
- 方法二:定時器
- 實現思想:
- 判斷當前是否有定時器,
- 沒有 就定義定時器,到規定時間執行,且清空定時器
- 有則不執行
function foo(func, wait) { var context, args; var timeout; return function() { if(!timeout){ setTimeout(()=>{ timeout = null; func.apply(context, args); }, wait) } } } 復制代碼
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