主頁 > 前端設計 > JavaScript學習筆記

JavaScript學習筆記

2021-10-23 08:48:06 前端設計

JavaScript學習筆記

  • 定義變數
    • let 與塊級作用域
      • 塊級作用域
    • const命令
  • 函式式編程
    • 為什么要學習函式式編程
    • 函式式編程的概念
    • JS中的函式
      • 函式是一等公民
      • 高階函式
        • 什么是高階函式
        • 常用高階函式
      • 閉包
    • 純函式
      • 純函式的好處
      • 副作用
    • 柯里化
      • lodash中的柯里化函式
    • 函式的組合
    • 箭頭函式
    • 引數默認值
    • 剩余引數
    • 展開陣列
  • 解構
    • 陣列解構
    • 物件的解構賦值
    • 字串的解構
  • 模板字串
    • 標簽模板
  • 字串的擴展方法
    • includes()
    • startsWith()
    • endsWidth()
  • 物件字面量增量
  • Symbol
    • 概述
    • Symbol.prototype.description
    • 作為屬性名的 Symbol
    • 消除魔法字串
    • 屬性名的遍歷
    • Symbol.for(),Symbol.keyFor()
    • 內置的 Symbol 值
  • Set
    • 基本用法
    • Set 實體的屬性和方法
    • 遍歷操作
    • WeakSet
  • Reflect
  • Promise
    • 概述
    • Promise物件的狀態
    • Promise建構式
    • Promise.prototype.then()
    • Promise.prototype.catch()
    • Promise.prototype.finally()
    • Promise.all()
    • Promise.allSettled()
    • Promise.race()
    • Promise.any()
    • Promise.resolve()
    • Promise.reject()
  • 宏任務和微任務
      • 運行機制
  • setTimeout和setTimeInterval
    • setTimeout()
    • setInterval()
      • clearTimeout(),clearInterval()
    • debounce函式
    • 運行機制
    • setTimeout(f, 0)
      • 含義
      • 應用
  • 物件的方法
    • Object.create
    • Object.assign
    • Object.is
    • Object.defineProperty
    • Object.prototype.isPrototypeOf()
    • Object.prototype.__proto__
  • Proxy
      • Proxy與Object.defineProperty
  • 面向物件
    • this指標
      • 使用場合
        • (1)全域環境
        • (2)函式內部
        • (3)建構式
        • (4)物件的方法
        • 原型鏈中的this
        • setTimeout和setInterval
        • 箭頭函式中的this
        • DOM事件處理函式
        • 行內事件
      • 系結this的方法
        • call,apply,bind三者的區別
        • Function.prototype.call()
        • Function.prototype.apply()
        • Function.prototype.bind()
    • 物件的繼承
      • 原型物件概述
        • 建構式的缺點
        • prototype 屬性的作用
        • 原型鏈
        • constructor 屬性
      • instanceof運算子
      • 建構式的繼承
      • 多重繼承
    • 建構式內的方法與原型上的方法的對比
    • ES6中的class
      • class的定義
      • 取值函式和存值函式
      • 靜態方法
      • 實體屬性的新寫法
      • 類的繼承
        • Object.getPrototypeOf()
        • super 關鍵字
  • Module模塊
    • Module 的加載實作
      • 瀏覽器加載
      • 加載規則
    • ES6 模塊與Common.js模塊的差異

定義變數

let 與塊級作用域

  • (1)let宣告的變數,只在let命令所在的代碼塊內有效,
    var在全域作用域和函式作用域內有效,
{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1
  • (2)for回圈計數器很適合let命令
var elements = [{},{},{}]
for(var i = 0; i<elements.length;i++){
    elements[i].onclick = function(){
        console.log(i)
    }
}

elements[0].onclick() //列印結果為3

上面變數I是var命令宣告的,在全域范圍內都有效,所以全域只有一個變數i,每一次回圈,變數i的值都會發生變化,而回圈內被賦值給數組a的函式內部的console.log(i)里面的i指向的是全域的i,也就是說,所有陣列a的成員里面的i,指向的都是同一個i,導致運行時輸出的最后一輪的i值,也就是3,

這也是立即執行函式的應用場景,

for(var i = 0; i<elements.length;i++){
    (function(i){
        elements[i].onclick = function(){
            console.log(i)
        }
    })(i)
}
elements[1].onclick() //列印結果為1

利用立即執行函式構造一個函式作用域,console.log(i)中的 i是函式作用域的i,

如果使用let,宣告的變數僅在塊級作用域內有效,

for(let i = 0; i<elements.length;i++){
    elements[i].onclick = function(){
        console.log(i)
    }
}

elements[0].onclick() //列印結果為0

變數i是let宣告的,當前的i只在本輪回圈有效,所以每一次回圈的i其實都是一個新的變數,

  • (3)不存在變數提升
    var命令會發生變數提升現象,即變數可以在宣告之前使用,值為undefined
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;

// let 的情況
console.log(bar); // 報錯ReferenceError
let bar = 2;
  • (4)內層變數可能會覆寫外層變數,
var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

函式f執行后,輸出結果為undefined,原因在于變數提升,函式作用域發生變數提升,

  • (5)暫時性死區
    只要塊級作用域記憶體在let命令,它所宣告的變數就系結在這個區域,不在受到外部的影響,
var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代碼中,存在全域變數tmp,但是塊級作用域內let又宣告了一個區域變數tmp,導致后者系結這個塊級作用域,所以在let宣告變數前,對tmp賦值會報錯,

  • (6)不允許重復宣告
    let不允許在相同作用域內,重復宣告同一個變數,
    var是可以在相同作用域內宣告同一個變數,
// 報錯
function func() {
  let a = 10;
  var a = 1;
}

// 報錯
function func() {
  let a = 10;
  let a = 1;
}

塊級作用域

ES5只有全域作用域和函式作用域,沒有塊級作用域,
ES6 let實際上為 JavaScript 新增了塊級作用域,

function f1() {
  let n = 5; //外層的塊級作用域
  if (true) { //塊級作用域
    let n = 10;
  }
  console.log(n); // 5
}

const命令

const宣告一個只讀的常量,一旦宣告,常量的值就不能改變,

const PI = 3.1415;

const其他的特性與let保持一致,

const實際上保證的,并不是變數的值不得改動,而是變數指向的那個記憶體地址所保存的資料不得改動,對于簡單型別的資料(數值、字串、布林值),值就保存在變數指向的那個記憶體地址,因此等同于常量,但對于復合型別的資料(主要是物件和陣列),變數指向的記憶體地址,保存的只是一個指向實際資料的指標,const只能保證這個指標是固定的(即總是指向另一個固定的地址),至于它指向的資料結構是不是可變的,就完全不能控制了,因此,將一個物件宣告為常量必須非常小心,

const foo = {};

// 為 foo 添加一個屬性,可以成功
foo.prop = 123;
foo.prop // 123

// 將 foo 指向另一個物件,就會報錯
foo = {}; // TypeError: "foo" is read-only

上面代碼中,常量foo儲存的是一個地址,這個地址指向一個物件,不可變的只是這個地址,即不能把foo指向另一個地址,但物件本身是可變的,所以依然可以為其添加新屬性,

函式式編程

為什么要學習函式式編程

  • 函式式編程是隨著React的流行受到越來越多的關注,
  • Vue 3也開始擁抱函式式編程
  • 函式式編程可以拋棄this
  • 打包程序中可以更好的利用tree shaking過濾無用代碼
  • 方便測驗,方便并行處理
  • 有很多庫可以幫助我們進行函式式開發: lodash、undersoore、ramda,

函式式編程的概念

面向物件編程的思維方式:把現實世界的事物抽象成程式世界中的類和物件,通過封裝、繼承和多型來演示事物事件的聯系,
函式式編程的思維方式:把現實世界的事物和事物之間的聯系抽象到程式世界(對運算程序進行抽象),

  • 程式的本質:根據輸入通過某種運算獲得相應的輸出,程式開發程序中會涉及很多有輸入和輸出的函式,
  • x->f(聯系、映射)->y,y=f(x)
  • 函式式編程中的函式指的不是程式中的函式(方法),而是數學中的函式即映射關系,例如:y=sin(x),x和y的關系,
  • 相同的輸入始終得到相同的輸出(純函式)
  • 函式式編程用來描述資料(函式)之間的映射,

JS中的函式

函式是一等公民

  • 函式可以存盤在變數中
  • 函式可以作為引數
  • 函式可以作為回傳值,

JavaScript中函式就是一個普通的物件(可以通過new Function()來創建),我們可以把函式存盤到變數或者陣列中,它還可以作為另外一個函式的引數和回傳值,甚至可以在程式運行的時候通過 new Function('alert(1)')來構造一個新的函式,

高階函式

什么是高階函式

  • 高階函式
    • 可以把函式作為引數傳遞給另外一個引數
    • 可以把函式作為另一函式的回傳結果,
  • 函式作為引數
//模擬forEach
function forEach(array,fn){
    for(let i = 0; i<array.length; i++){
        fn(array[i])
    }
}

//模擬filter 
function filter(array,fn){
    let results = []
    for(let i = 0; i<array.length; i++){
        if(fn(array[i])){
            results.push(array[i])
        }
    }

    return results
}
  • 函式作為回傳值
function makeFn(){
    let msg ='Hello function'
    return function(){
        console.log(msg)
    }
}
const fn = makeFn()
fn();
//模擬once
function once(fn){
    let done = false 
    return function(){
        if(!done){
            done = true
            fn.apply(this,arguments)
        }
    }
}

let pay = once(function(money){
    console.log(`支付:${money}RMB`);
})
//只會支付一次
pay(5)
pay(5)
pay(5)

常用高階函式

map、filter、reduce、forEatch、every、some、find/findIndex、sort…

1、map
map 是映射的意思,
原陣列被映射成一個新的陣列,回傳值是一個新陣列,不改變原來的陣列,新的陣列與原陣列的長度是不會改變的,

//item:陣列當前項的值
//index:陣列當前項的索引
//self:陣列物件本身 
let array = [1, 2, 3, 4, 5]
array.map((item, index, self) => {
	console.log(item)
	console.log(index)
	console.log(self)
})

示例:

  let arr = [1,2,3,4,5]
    let newArr = arr.map((item)=>{
        return item*item;
    })
    console.log(newArr) //[1, 4, 9, 16, 25]

上述實體,map接收的引數是一個函式,該函式依次作用于每個元素,對元素放大了2倍,也可以對其進行任意的復雜操作,

2、filter
filter 是過濾陣列,回傳滿足條件的資料,組成一個新的陣列回傳,不滿足條件的被丟棄,

	//item:陣列當前項的值
	//index:陣列當前項的索引
	//self:陣列物件本身 
   let arr = [1,2,3,4,5]
   let newArr = arr.filter((item,index,self)=>{
       console.log(item)
       console.log(index)
       console.log(array)
   })

示例:

   let arr = [1,2,3,4,5]
   let newArr = arr.filter(item=>{
        return item % 2 == 0
   })
   console.log(newArr) //[2,4]

上述實體中,filter傳入的引數是一個函式,傳入的函式依次作用于每個元素,然后根據回傳值是 true 或 false 決定保留還是丟棄元素,因為只有 2和 4 兩個滿足條件,所以新的陣列中只有這兩個元素,

利用filter給陣列去重

  //利用filter給陣列去重
  let arr = [1,2,3,4,5,5,4,3,2,1]
  let newArr = arr.filter((item,index,arr)=>{
      //array.indexOf()回傳的是陣列的此元素的第一個索引,
      return arr.indexOf(item) === index
  })
  console.log(newArr) //[1,2,3,4,5]

3、reduce
reduce 是對陣列進行匯總的,往往進去一個陣列,出來是一個資料,經常用于求和和計算平均值,

	//tmp: 臨時值,暫存
	//item:陣列當前項的值
	//index:陣列當前項的索引
    let arr = [1,2,3,4,5]
    let result = arr.reduce((tmp,item,index)=>{
        console.log('index:' + index+','+'item:'+item+'tmp:'+tmp)
       return tmp + item
    })
    /*
        列印結果:
        index:1,item:2tmp:1
        index:2,item:3tmp:3
        index:3,item:4tmp:6
        index:4,item:5tmp:10
    */
    //result=15

4、forEach
遍歷陣列,

	//item:陣列當前項的值
	//index:陣列當前項的索引
	//self:陣列物件本身 
    let arr = [1,2,3,4,5]
    arr.forEach((item,index,self)=>{
        console.log(item)
        console.log(index)
        console.log(array)
    })

5、every
every()是對陣列中每一項運行給指定函式,如果該函式對每一項回傳true,則回傳true,

  • 1.如果陣列中檢測到有一個元素不滿足,則整個運算式回傳 false ,且剩余的元素不會再進行檢測,
  • 2.如果所有元素都滿足條件,則回傳 true,
	//item:陣列當前項的值
	//index:陣列當前項的索引
	//self:陣列物件本身 
    let arr = [3,4,5]
    let result = arr.every((item,index,self)=>{
        return item > 2
    })
    console.log(result) //true

6、some()
some()是對陣列中每一項運行給定函式,如果該函式對任一項回傳true,則回傳true,

  • 1.如果有一個元素滿足條件,則運算式回傳true , 剩余的元素不會再執行檢測,
  • 2.如果沒有滿足條件的元素,則回傳false,
	//item:陣列當前項的值
	//index:陣列當前項的索引
	//self:陣列物件本身 
	let arr = [3,4,5]
    let result = arr.some((item,index,self)=>{
        return item > 4
    })
    console.log(result) //true

7、find()
find() 方法回傳通過測驗(函式內判斷)的陣列的第一個元素的值,

  • 如果沒有符合條件的元素回傳 undefined
  • find() 對于空陣列,函式是不會執行的,
  • find() 并沒有改變陣列的原始值,
	//item:陣列當前項的值
	//index:陣列當前項的索引
	//self:陣列物件本身 
    let arr = [3,4,5,6]
    let result = arr.find((item,index,self)=>{
        return item % 2 == 0 
    })
    console.log(result) //4

8、findIndex()
回傳滿足回呼函式中指定的測驗條件的第一個陣列元素的索引值,

  • 對于陣列中的每個元素,findIndex 方法都會呼叫一次回呼函式(采用升序索引順序),直到有元素回傳 true,
  • 只要有一個元素回傳 true,findIndex 立即回傳該回傳 true 的元素的索引值,
  • 如果陣列中沒有任何元素回傳 true,則 findIndex 回傳 -1,
	//item:陣列當前項的值
	//index:陣列當前項的索引
	//self:陣列物件本身 
    let arr = [3,4,5,6]
    let firstIndex = arr.findIndex((item,index,self)=>{
         return item % 2 == 0 
    })
    console.log(firstIndex) //1

9、sort()
sort()方法按升序排列陣列,

    let arr = [4,3,5,6]
    let newArr = arr.sort()
    console.log(newArr) //[3,4,5,6]

這時發現資料按照從小到大排列,沒問題;于是再把陣列改成:let arr=[101,1,3,5,9,4,11];,再呼叫sort()方法列印排序結果,

var arr=[101,1,3,5,9,4,11];
console.log(arr.sort());
// 輸出: [1, 101, 11, 3, 4, 5, 9]

這個時候發現陣列101,11都排在3前面,是因為 sort() 方法會呼叫陣列的toString()轉型方法,然后比較得到的字串,確定如何排序,即使陣列中的每一項都是數值,sort()方法比較的也是字串,
那么字串又是怎么排序的呢,是根據字串的unicode編碼從小到大排序的,下面我們嘗試列印出陣列每一項的unicode編碼看一下,

...
// 轉碼方法
function getUnicode (charCode) {
    return charCode.charCodeAt(0).toString(16);
}
// 列印轉碼
arr.forEach((n)=>{
  console.log(getUnicode(String(n)))
});

// 輸出: 31 31 31 33 34 35 39

驚奇地發現,1,101,11的字串unicode編碼都是31
以上發現sort()方法不是按照我們想要的順序排序的,那么,怎么解決呢,sort()方法可以接收一個比較函式作為引數,以便指定哪個值位于哪個值前面,

比較函式(compare)接收兩個引數,如果第一個引數位于第二個之前則回傳一個負數,如果兩個引數相等則回傳0,如果第一個引數位于第二個之后則回傳一個整數,
function compare(value1,value2){
  if (value1 < value2){
    return -1;
  } else if (value1 > value2){
    return 1;
  } else{
    return 0;
  }
}

我們把比較函式傳遞給sort()方法,在對arr陣列進行排列,列印結果如下:
var arr=[101,1,3,5,9,4,11];
console.log(arr.sort(compare));
// 輸出: [1, 3, 4, 5, 9, 11, 101];
  • 物件陣列的排序
    ?sort() 方法通過傳入一個比較函式來排序數字陣列,但是在開發中,我們會對一個物件陣列的某個屬性進行排序,例如id,年齡等等,那么怎么解決呢?
    要解決這個問題:我們可以定義一個函式,讓它接收一個屬性名,然后根據這個屬性名來創建一個比較函式并作為回傳值回傳來,代碼如下,
function compareFunc(prop){
  return function (obj1,obj2){
    var value1=obj1[prop];
    var value2=obj2[prop];
    if (value1 < value2){
        return -1;
    } else if (value1 > value2){
        return 1;
    } else{
        return 0;
    }
  }
}

定義一個陣列users,呼叫sort()方法傳入compareFunc(prop)列印輸出結果:

var users=[
    {name:'tom',age:18},
	{name:'lucy',age:24},
    {name:'jhon',age:17},
];
console.log(users.sort(compareFunc('age')));
// 輸出結果
[{name: "jhon", age: 17},
{name: "tom", age: 18},
{name: "lucy", age: 24}]

在默認情況下,呼叫sort()方法不傳入比較函式時,sort()方法會呼叫每個物件的toString()方法來確定他們的次序,當我們呼叫compareFunc('age')方法創建一個比較函式,排序是按照物件的age屬性排序的,

閉包

  • 閉包:函式和周圍的狀態(詞法環境)的參考綁在一起形成閉包,
  • 閉包指有權訪問另一個函式作用域中的變數的函式,一般情況就是一個函式中包含另外一個函式,
  • 閉包的作用:訪問函式內部的變數、保持函式在環境中一直存在,不會被垃圾回識訓制處理,
//模擬once
function once(fn){
    let done = false 
    return function(){
        if(!done){
            done = true
            fn.apply(this,arguments)
        }
    }
}
  • 閉包的本質:函式在執行的時候會放到一個執行堆疊上當函式執行完畢之后會從執行堆疊上移除,但是堆上的作用域成員因為被外部參考不能釋放,因此內部函式依然可以訪問外部的成員,
  • 優缺點:
    優點:能夠讓希望一個變數長期駐扎在記憶體之中成為可能,避免全域變數的污染,以及允許私有成員的存在,
    缺點: 就是常駐記憶體會增大記憶體使用量,并且使用不當容易造成記憶體泄漏,

如果不是因為某些特殊任務而需要閉包,在沒有必要的情況下,在其他函式中創建函式是不明智的,因為閉包對腳本性能具有負面影響,包括處理速度和記憶體消耗,

純函式

  • 純函式:相同的輸入永遠會得到相同的輸出,而且沒有任何副作用,
  • 函式式編程中的函式就是純函式,
  • lodash是一個純函式的功能庫,提供了對陣列、數字、物件、字串、函式等操作的一些方法,
  • 陣列的slice和splice分別是純函式和不純函式,
    slice回傳陣列中的指定部分,不會改變原陣列,
    splice對陣列進行操作并且回傳該陣列,會改變原陣列,

函式式編程不會保留計算中間的結果,所以變數是不可變的(無狀態的)
我們可以把一個函式的執行結果交給另一個函式去處理,

純函式的好處

可快取
因為純函式對相同的輸入始終有相同的結果,所以可以把純函式的結果快取起來,

可測驗
純函式讓測驗更加方便

并行處理
在多執行緒環境下并行操作共享的記憶體資料很可能會出現意外情況
純函式不需要訪問共享的記憶體資料,所以在并行環境下可以任意運行純函式,

副作用

  • 純函式:對于相同輸入永遠會得到相同的輸出,而且沒有任何可觀察的副作用,
//不純的
let mini = 19
function checkAge(age) {
    return age >= mini
}

//純的(有硬編碼,后續可以通過柯里化解決)
function checkAge(age){
    let mini = 19
    return age >= mini
}

副作用讓一個函式變的不純,純函式根據相同的輸入回傳相同的輸出,如果函式依賴于外部的狀態就無法保證輸出相同,就會帶來副作用,

副作用來源:
(1)組態檔
(2)資料庫
(3)獲取用戶的輸入

所有的外部互動都有可能帶來副作用,副作用也使得方法通用性下降不適合擴展和可重用性,同時副作
用會給程式中帶來安全隱患給程式帶來不確定性,但是副作用不可能完全禁止,盡可能控制它們在可控
范圍內發生,

柯里化

//不純的
let mini = 19
function checkAge(age) {
    return age >= mini
}

//純的(有硬編碼,后續可以通過柯里化解決)
function checkAge(mini,age){
    return age >= mini
}

checkAge(18,20)

//柯里化
function checkAge(mini){
    return function(age){
        return age >= mini
    }
}
//ES6的寫法:
let checkAge = mini=>(age=>age>=mini)

//呼叫:
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
console.log(checkAge20(50))

柯里化:
當一個函式有多個引數的時候先傳遞一部分引數呼叫它(這部分引數以后永遠不變)
然后回傳一個新的函式接收剩余的引數,回傳結果,

lodash中的柯里化函式

  • _.curry(func)
    功能:創建一個函式,該函式接收一個或者多個func的引數,如果func所需要的引數都被提供則執行func并且回傳執行的結果,否則繼續回傳該函式并等待接收剩余的引數,
    引數:需要柯里化的函式
    回傳值:柯里化后的函式
//要柯里化的函式

function getSum(a,b,c) {
    return a + b + c 
}
//柯里化后的函式
let curried = _.curry(getSum)
//測驗:
curried(1,2,3)
curried(1)(2)(3)
curried(1,2)(3)

總結:

  • 柯里化可以讓我們給一個函式傳遞較少的引數得到一個已經記住了某些固定引數的新函式,
  • 這是一種對函式引數的快取
  • 讓函式變的更加靈活,讓函式的顆粒度更少,
  • 可以把多元函式轉換成一元函式,可以組合使用函式產生強大的功能,

函式的組合

  • 純函式和柯里化很容易寫出洋蔥代碼h(g(f(x)))
  • 獲取陣列的最后一個元素再轉換成大寫字母_.toUpper(_.first(_.reverse(array)))

函陣列合可以讓我們把細粒度的函式重新組合生成一個新的函式,

函陣列合:如果一個函式要經過多個函式處理才能得到最終的結果,這個時候可以把中間程序的函式合并成為一個函式,
函陣列合默認是從右邊到左邊執行,

function compose(f,g){
    return function(x){
        return f(g(x))
    }
}

function first(arr){
    return arr[0]
}
function reverse(arr) {
    return arr.reverse()
}

//從右邊到左邊運行
let last = compose(first,reverse)
console.log(last([1,2,3,4]))
  • lodash 中的組合函式
  • lodash中組合函式flow()或者flowRight(),他們都可以組合多個函式,
  • flow()是從左到右邊執行
  • flowRight()是從右到左執行,使用的更多一些,
const _ = require('lodash');
const toUpper = s=>s.toUpperCase()
const reverse = arr=>arr.reverse()
const first = arr=>arr.first()

const f = _.flowRight(toUpper,first,reverse)
console.log(f(['one','two','trhee']))

箭頭函式

ES6 允許使用“箭頭”(=>)定義函式,

var f = v => v;

// 等同于
var f = function (v) {
  return v;
};

如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分,

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭頭函式的代碼塊部分多于一條陳述句,就要使用大括號將它們括起來,并且使用return陳述句回傳,

var sum = (num1, num2) => { return num1 + num2; }

引數默認值

ES 5設定引數默認值

function foo(enable) {
    enable = enable === undefined ? true : enable
}

ES 6設定引數默認值

function foo(bar,enable = true) {
}

剩余引數

以前都是通過arguments物件來獲取剩余引數.
ES6 引入 rest 引數(形式為…變數名),用于獲取函式的多余引數,這樣就不需要使用arguments物件了,rest 引數搭配的變數是一個陣列,該變數將多余的引數放入陣列中,

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

展開陣列

let arr = [1,2,3]
console.log(...arr) //1,2,3

解構

陣列解構

ES6 允許按照一定模式,從陣列和物件中提取值,對變數進行賦值,這被稱為解構(Destructuring),

  • 解構陣列
let [a, b, c] = [1, 2, 3];
  • 提取從當前位置開始的所有成員,
const arr = [100,200,300]
const [foo,...rest] = arr
console.log(rest) //列印結果[200,300]

這種三個點的用法只能在解構位置的最后的位置使用,

  • 如果解構不成功,變數的值就等于undefined,
let [foo] = [];
let [bar, foo] = [1];
  • 不完全解構,即等號左邊的模式,只匹配一部分的等號右邊的陣列,這種情況下,解構依然可以成功,
let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
  • 提取指定位置的成員
const arr = [100,200,300]
const [,,last] = arr
console.log(last)
  • 解構賦值允許指定默認值,
let [foo = true] = [];
foo // true

本質上,這種寫法屬于“模式匹配”,只要等號兩邊的模式相同,左邊的變數就會被賦予對應的值,

物件的解構賦值

  • 解構屬性
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

物件的解構與陣列有一個重要的不同,陣列的元素是按次序排列的,變數的取值由它的位置決定;而物件的屬性沒有次序,變數必須與屬性同名,才能取到正確的值,

  • 也可以解構方法
let {log} = console
log("HelloWorld!")

字串的解構

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

模板字串

傳統字串不支持換行,但是模板字串支持多行,

// 普通字串
`In JavaScript '\n' is a line-feed.`

// 多行字串
$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

支持通過插值運算式嵌入內容

// 字串中嵌入變數
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

標簽模板

它可以緊跟在一個函式名后面,該函式將被呼叫來處理這個模板字串,這被稱為“標簽模板”功能

alert`hello`
等同于
alert(['hello']) //是一個陣列

標簽模板其實不是模板,而是函式呼叫的一種特殊形式,“標簽”指的就是函式,緊跟在后面的模板字串就是它的引數,

字串的擴展方法

includes()

字串中間是否包含,

message.includes('foo') //字串中間是否包含

startsWith()

const message = ` Error: foo is not defined.`
message.startsWith('Error') //是否以Error開頭

endsWidth()

message.endsWith('.')//是否以點結尾

物件字面量增量

傳統的物件字面量,

const obj = {
    foo:123,
    bar:bar,
}

增強之后:

const obj = {
    foo:123,
    bar
}

傳統物件字面量-方法

const obj = {
    foo:123,
    method:function(){
        console.log("methods")
    }
}

增強之后

const obj = {
    foo:123,
    method(){
        console.log("methods")
    }
}

Symbol

概述

ES5 的物件屬性名都是字串,這容易造成屬性名的沖突,比如,你使用了一個他人提供的物件,但又想為這個物件添加新的方法(mixin 模式),新方法的名字就有可能與現有方法產生沖突,如果有一種機制,保證每個屬性的名字都是獨一無二的就好了,這樣就從根本上防止屬性名的沖突,這就是 ES6 引入Symbol的原因,

ES6 引入了一種新的原始資料型別Symbol表示獨一無二的值,它是 JavaScript 語言的第七種資料型別,前六種是:undefinednull、布林值(Boolean)、字串(String)、數值(Number)、物件(Object),

Symbol 值通過Symbol函式生成,這就是說,物件的屬性名現在可以有兩種型別,**一種是原來就有的字串,另一種就是新增的 Symbol 型別,**凡是屬性名屬于 Symbol 型別,就都是獨一無二的,可以保證不會與其他屬性名產生沖突,

注意,Symbol函式前不能使用new命令,否則會報錯,這是因為生成的 Symbol 是一個原始型別的值,不是物件,也就是說,由于 Symbol 值不是物件,所以不能添加屬性,基本上,它是一種類似于字串的資料型別,

Symbol函式可以接受一個字串作為引數,表示對 Symbol 實體的描述,主要是為了在控制臺顯示,或者轉為字串時,比較容易區分,

// 沒有引數的情況
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有引數的情況
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

Symbol.prototype.description

ES2019 提供了一個實體屬性description,直接回傳 Symbol 的描述,

const sym = Symbol('foo');

String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"
sym.description // "foo"

作為屬性名的 Symbol

由于每一個 Symbol 值都是不相等的,這意味著 Symbol 值可以作為識別符號,用于物件的屬性名,就能保證不會出現同名的屬性,這對于一個物件由多個模塊構成的情況非常有用,能防止某一個鍵被不小心改寫或覆寫,

let mySymbol = Symbol();

// 第一種寫法
let a = {};
a[mySymbol] = 'Hello!';

// 第二種寫法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三種寫法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"

注意,Symbol 值作為物件屬性名時,不能用點運算子,

消除魔法字串

魔術字串指的是,在代碼之中多次出現、與代碼形成強耦合的某一個具體的字串或者數值,風格良好的代碼,應該盡量消除魔術字串,改由含義清晰的變數代替,

function getArea(shape, options) {
  let area = 0;

  switch (shape) {
    case 'Triangle': // 魔術字串
      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }

  return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔術字串

上面代碼中,字串Triangle就是一個魔術字串,它多次出現,與代碼形成“強耦合”,不利于將來的修改和維護,

常用的消除魔術字串的方法,就是把它寫成一個變數,

const shapeType = {
  triangle: 'Triangle'
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

上面代碼中,我們把Triangle寫成shapeType物件的triangle屬性,這樣就消除了強耦合,

如果仔細分析,可以發現shapeType.triangle等于哪個值并不重要,只要確保不會跟其他shapeType屬性的值沖突即可,因此,這里就很適合改用 Symbol 值,

const shapeType = {
  triangle: Symbol()
};

上面代碼中,除了將shapeType.triangle的值設為一個 Symbol,其他地方都不用修改,

屬性名的遍歷

Symbol 作為屬性名,遍歷物件的時候,該屬性不會出現在for...infor...of回圈中,也不會被Object.keys()Object.getOwnPropertyNames()JSON.stringify()回傳

但是,它也不是私有屬性,有一個Object.getOwnPropertySymbols()方法,可以獲取指定物件的所有 Symbol 屬性名,該方法回傳一個陣列,成員是當前物件的所有用作屬性名的 Symbol 值,

const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]

由于以 Symbol 值作為鍵名,不會被常規方法遍歷得到,我們可以利用這個特性,為物件定義一些非私有的、但又希望只用于內部的方法,

Symbol.for(),Symbol.keyFor()

有時,我們希望重新使用同一個 Symbol 值,Symbol.for()方法可以做到這一點,它接受一個字串作為引數,然后搜索有沒有以該引數作為名稱的 Symbol 值,如果有,就回傳這個 Symbol 值,否則就新建一個以該字串為名稱的 Symbol 值,并將其注冊到全域,

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.for()Symbol()這兩種寫法,都會生成新的 Symbol它們的區別是,前者會被登記在全域環境中供搜索,后者不會Symbol.for()不會每次呼叫就回傳一個新的 Symbol 型別的值,而是會先檢查給定的key是否已經存在,如果不存在才會新建一個值,

Symbol.keyFor()方法回傳一個已登記的 Symbol 型別值的key,

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

上面代碼中,變數s2屬于未登記的 Symbol 值,所以回傳undefined,

內置的 Symbol 值

Set

基本用法

ES6 提供了新的資料結構 Set,它類似于陣列,但是成員的值都是唯一的,沒有重復的值,
Set本身是一個建構式,用來生成 Set 資料結構,

Set函式可以接受一個陣列(或者具有 iterable 介面的其他資料結構)作為引數,用來初始化,可以使用Set的特性實作陣列去重,

    let arr = [1,2,3,4,6,6,6]
    let set = new Set(arr) //去掉重復的數值
    let newArr = [...set] //展開set形成一個新的陣列

	// 去除陣列的重復成員
	[...new Set(arr)]

上面的方法也可以用于,去除字串里面的重復字符,

[...new Set('ababbc')].join('')
// "abc"

向 Set 加入值的時候,不會發生型別轉換,所以5和"5"是兩個不同的值,Set 內部判斷兩個值是否不同,使用的演算法叫做“Same-value-zero equality”,它類似于精確相等運算子(===),主要的區別是向 Set 加入值時認為NaN等于自身,而精確相等運算子認為NaN不等于自身,

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}

上面代碼向 Set 實體添加了兩次NaN,但是只會加入一個,這表明,在 Set 內部,兩個NaN是相等的,
另外,兩個物件總是不相等的,

let set = new Set();

set.add({});
set.size // 1

set.add({});
set.size // 2

上面代碼表示,由于兩個空物件不相等,所以它們被視為兩個值,

Set 實體的屬性和方法

  • Set.prototype.constructor:建構式,默認就是Set函式,
  • Set.prototype.size:回傳Set實體的成員總數,
  • Set.prototype.add(value):添加某個值,回傳 Set 結構本身,
  • Set.prototype.delete(value):洗掉某個值,回傳一個布林值,表示洗掉是否成功,
  • Set.prototype.has(value):回傳一個布林值,表示該值是否為Set的成員,
  • Set.prototype.clear():清除所有成員,沒有回傳值,

遍歷操作

  • Set.prototype.keys():回傳鍵名的遍歷器
  • Set.prototype.values():回傳鍵值的遍歷器
  • Set.prototype.entries():回傳鍵值對的遍歷器
  • Set.prototype.forEach():使用回呼函式遍歷每個成員
    需要特別指出的是,Set的遍歷順序就是插入順序,這個特性有時非常有用,比如使用 Set 保存一個回呼函式串列,呼叫時就能保證按照添加順序呼叫,

WeakSet

WeakSet 結構與 Set 類似,也是不重復的值的集合,但是,它與 Set 有兩個區別,

首先,WeakSet 的成員只能是物件,而不能是其他型別的值,

其次,WeakSet 中的物件都是弱參考,即垃圾回識訓制不考慮 WeakSet 對該物件的參考,也就是說,如果其他物件都不再參考該物件,那么垃圾回識訓制會自動回收該物件所占用的記憶體,不考慮該物件還存在于 WeakSet 之中,

是因為垃圾回識訓制根據物件的可達性(reachability)來判斷回收,如果物件還能被訪問到,垃圾回識訓制就不會釋放這塊記憶體,結束使用該值之后,有時會忘記取消參考,導致記憶體無法釋放,進而可能會引發記憶體泄漏,WeakSet 里面的參考,都不計入垃圾回識訓制,所以就不存在這個問題,因此,WeakSet 適合臨時存放一組物件,以及存放跟物件系結的資訊,只要這些物件在外部消失,它在 WeakSet 里面的參考就會自動消失,

由于上面這個特點,WeakSet 的成員是不適合參考的,因為它會隨時消失,另外,由于 WeakSet 內部有多少個成員,取決于垃圾回識訓制有沒有運行,運行前后很可能成員個數是不一樣的,而垃圾回識訓制何時運行是不可預測的,因此 ES6 規定 WeakSet 不可遍歷,

Reflect

屬于一個靜態類,統一的操作物件的API,

Promise

概述

Promise 物件是 JavaScript 的異步操作解決方案,為異步操作提供統一介面,它起到代理作用(proxy),充當異步操作與回呼函式之間的中介,使得異步操作具備同步操作的介面,

Promise 的設計思想是,所有異步任務都回傳一個 Promise 實體,Promise 實體有一個then方法,用來指定下一步的回呼函式,

Promise物件的狀態

Promise 物件通過自身的狀態,來控制異步操作,Promise 實體具有三種狀態,

  • 異步操作未完成(pending)
  • 異步操作成功(fulfilled)
  • 異步操作失敗(rejected)

Promise建構式

JavaScript 提供原生的Promise建構式,用來生成 Promise 實體,

const promise = new Promise(function (resolve, reject) {
  // ...

  if (/* 異步操作成功 */){
    resolve(value);
  } else { /* 異步操作失敗 */
    reject(new Error());
  }
});

上面代碼中,Promise建構式接受一個函式作為引數,該函式的兩個引數分別是resolvereject,它們是兩個函式,由 JavaScript 引擎提供,不用自己實作,

resolve函式的作用是,將Promise實體的狀態從“未完成”變為“成功”(即從pending變為fulfilled),在異步操作成功時呼叫,并將異步操作的結果,作為引數傳遞出去,reject函式的作用是,將Promise實體的狀態從“未完成”變為“失敗”(即從pending變為rejected),在異步操作失敗時呼叫,并將異步操作報出的錯誤,作為引數傳遞出去,

Promise.prototype.then()

Promise 實體具有then方法,也就是說,then方法是定義在原型物件Promise.prototype上的,它的作用是為 Promise 實體添加狀態改變時的回呼函式,then方法的第一個引數是resolved狀態的回呼函式,第二個引數是rejected狀態的回呼函式,它們都是可選的,

promise.then(function(value){
    console.log("異步操作成功的回呼")
},function(error){
    console.log("異步操作失敗的回呼")
})

then方法可以鏈式使用,

p1
  .then(step1)
  .then(step2)
  .then(step3)
  .then(
    console.log,
    console.error
  );

上面代碼中,p1后面有四個then,意味依次有四個回呼函式,只要前一步的狀態變為fulfilled,就會依次執行緊跟在后面的回呼函式,

console.log只顯示step3的回傳值,而console.error可以顯示p1step1step2step3之中任意一個發生的錯誤,舉例來說,如果step1的狀態變為rejected,那么step2step3都不會執行了(因為它們是resolved的回呼函式),Promise 開始尋找,接下來第一個為rejected的回呼函式,在上面代碼中是console.error,這就是說,Promise 物件的報錯具有傳遞性,

Promise.prototype.catch()

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的別名,用于指定發生錯誤時的回呼函式,

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 處理 getJSON 和 前一個回呼函式運行時發生的錯誤
  console.log('發生錯誤!', error);
});

上面代碼中,getJSON()方法回傳一個 Promise 物件,如果該物件狀態變為resolved,則會呼叫then()方法指定的回呼函式;如果異步操作拋出錯誤,狀態就會變為rejected,就會呼叫catch()方法指定的回呼函式,處理這個錯誤,另外,then()方法指定的回呼函式,如果運行中拋出錯誤,也會被catch()方法捕獲,

// 寫法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 寫法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

Promise.prototype.finally()

finally()方法用于指定不管 Promise 物件最后狀態如何,都會執行的操作,該方法是 ES2018 引入標準的,

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代碼中,不管promise最后的狀態,在執行完then或catch指定的回呼函式以后,都會執行finally方法指定的回呼函式,

Promise.all()

Promise.all()方法用于將多個 Promise 實體,包裝成一個新的 Promise 實體,

const p = Promise.all([p1, p2, p3]);

(1)只有p1、p2、p3的狀態都變成fulfilledp的狀態才會變成fulfilled,此時p1、p2、p3的回傳值組成一個陣列,傳遞給p的回呼函式,
(2)只要p1、p2、p3之中有一個被rejectedp的狀態就變成rejected,此時第一個被reject的實體的回傳值,會傳遞給p的回呼函式,

// 生成一個Promise物件的陣列
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

Promise.allSettled()

有時候,我們希望等到一組異步操作都結束了,不管每一個操作是成功還是失敗,再進行下一步操作,但是,現有的 Promise 方法很難實作這個要求,

Promise.all()方法只適合所有異步操作都成功的情況,如果有一個操作失敗,就無法滿足要求,

為了解決這個問題,ES2020 引入了Promise.allSettled()方法,用來確定一組異步操作是否都結束了(不管成功或失敗),所以,它的名字叫做”Settled“,包含了”fulfilled“和”rejected“兩種情況,

Promise.allSettled()方法接受一個陣列作為引數,陣列的每個成員都是一個 Promise 物件,并回傳一個新的 Promise 物件,只有等到引數陣列的所有 Promise 物件都發生狀態變更(不管是fulfilled還是rejected),回傳的 Promise 物件才會發生狀態變更,

const promises = [
  fetch('/api-1'),
  fetch('/api-2'),
  fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator();

上面示例中,陣列promises包含了三個請求,只有等到這三個請求都結束了(不管請求成功還是失敗),removeLoadingIndicator()才會執行,
該方法回傳的新的 Promise 實體,一旦發生狀態變更,狀態總是fulfilled,不會變成rejected,狀態變成fulfilled后,它的回呼函式會接收到一個陣列作為引數,該陣列的每個成員對應前面陣列的每個 Promise 物件,
results的每個成員是一個物件,物件的格式是固定的,對應異步操作的結果,

// 異步操作成功時
{status: 'fulfilled', value: value}

// 異步操作失敗時
{status: 'rejected', reason: reason}

成員物件的status屬性的值只可能是字串fulfilled或字串rejected,用來區分異步操作是成功還是失敗,如果是成功(fulfilled),物件會有value屬性,如果是失敗(rejected),會有reason屬性,對應兩種狀態時前面異步操作的回傳值,

下面是回傳值的用法例子,

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);

// 過濾出成功的請求
const successfulPromises = results.filter(p => p.status === 'fulfilled');

// 過濾出失敗的請求,并輸出原因
const errors = results
  .filter(p => p.status === 'rejected')
  .map(p => p.reason);

Promise.race()

Promise.race()方法同樣是將多個 Promise 實體,包裝成一個新的 Promise 實體,

const p = Promise.race([p1, p2, p3]);

上面代碼中,只要p1、p2、p3之中有一個實體率先改變狀態,p的狀態就跟著改變,那個率先改變的 Promise 實體的回傳值,就傳遞給p的回呼函式,

Promise.race()方法的引數與Promise.all()方法一樣,如果不是 Promise 實體,就會先呼叫下面講到的Promise.resolve()方法,將引數轉為 Promise 實體,再進一步處理,
下面是一個例子,如果指定時間內沒有獲得結果,就將 Promise 的狀態變為reject,否則變為resolve,

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

上面代碼中,如果 5 秒之內fetch方法無法回傳結果,變數p的狀態就會變為rejected,從而觸發catch方法指定的回呼函式,

Promise.any()

ES2021 引入了Promise.any()方法,該方法接受一組 Promise 實體作為引數,包裝成一個新的 Promise 實體回傳,

Promise.any([
  fetch('https://v8.dev/').then(() => 'home'),
  fetch('https://v8.dev/blog').then(() => 'blog'),
  fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {  // 只要有一個 fetch() 請求成功
  console.log(first);
}).catch((error) => { // 所有三個 fetch() 全部請求失敗
  console.log(error);
});

只要引數實體有一個變成fulfilled狀態,包裝實體就會變成fulfilled狀態;如果所有引數實體都變成rejected狀態,包裝實體就會變成rejected狀態,

Promise.any()跟Promise.race()方法很像,只有一點不同,就是Promise.any()不會因為某個 Promise 變成rejected狀態而結束,必須等到所有引數 Promise 變成rejected狀態才會結束,

const promises = [
  fetch('/endpoint-a').then(() => 'a'),
  fetch('/endpoint-b').then(() => 'b'),
  fetch('/endpoint-c').then(() => 'c'),
];

try {
  const first = await Promise.any(promises);
  console.log(first);
} catch (error) {
  console.log(error);
}

上面代碼中,Promise.any()方法的引數陣列包含三個 Promise 操作,其中只要有一個變成fulfilled,Promise.any()回傳的 Promise 物件就變成fulfilled,如果所有三個操作都變成rejected,那么await命令就會拋出錯誤,

Promise.any()拋出的錯誤,不是一個一般的 Error 錯誤物件,而是一個 AggregateError 實體,它相當于一個陣列,每個成員對應一個被rejected的操作所拋出的錯誤,

Promise.resolve()

有時需要將現有物件轉為 Promise 物件,Promise.resolve()方法就起到這個作用,

const jsPromise = Promise.resolve($.ajax('/whatever.json'));

上面代碼將 jQuery 生成的deferred物件,轉為一個新的 Promise 物件,

Promise.resolve('foo')
// 等價于
new Promise(resolve => resolve('foo'))

Promise.reject()

Promise.reject(reason)方法也會回傳一個新的 Promise 實體,該實體的狀態為rejected,

const p = Promise.reject('出錯了');
// 等同于
const p = new Promise((resolve, reject) => reject('出錯了'))

p.then(null, function (s) {
  console.log(s)
});
// 出錯了

上面代碼生成一個 Promise 物件的實體p,狀態為rejected,回呼函式會立即執行,

宏任務和微任務

javascript中的異步任務包含兩種:宏任務和微任務,

宏任務:DOM事件回呼、AJAX事件回呼、定時器回呼,
微任務:Promise、MutationObserver、(node環境中還包括process.nextTick,)

注意:new Promise在實體化的程序中所執行的代碼都是同步進行的,而then中注冊的回呼才是異步執行的,
async/await底層是基于Promise封裝的,所以await前面的代碼相當于new Promise,是同步進行的,await后面的代碼相當于then,才是異步進行的,

js運行程式代碼是同步的,執行完所有同步代碼后就執行異步代碼;異步代碼中先執行微任務再執行宏任務,

異步任務執行順序:微任務→宏任務→微任務→宏任務……

運行機制

**Event Loop:**JavaScript是單執行緒腳本語言,同一時間不能處理多個任務,所以何時執行宏任務,何時執行微任務?我們需要有這樣的一個判斷邏輯存在,這個判斷邏輯被稱為事件回圈,

在這里插入圖片描述
事件回圈的程序如下:
1、JS引擎(唯一主執行緒)按順序決議代碼,遇到函式宣告,直接跳過,遇到函式呼叫,入堆疊,
2、如果是同步函式呼叫,直接執行得到結果,同步函式彈出堆疊,繼續下一個函式呼叫,
3、如果是異步函式呼叫,分發給Web API(多個輔助執行緒),異步函式彈出堆疊,繼續下一個函式呼叫,
4、Web API中,異步函式在相應輔助執行緒中處理完成后,即異步函式達到觸發條件(比如setTimeout設定的10s后),如果異步函式是宏任務,則入宏任務訊息佇列,如果是微任務,則入微任務訊息佇列,
5、Event Loop不停地檢查主執行緒的呼叫堆疊與回呼佇列,當呼叫堆疊為空時,就把微任務訊息佇列的第一個任務推入堆疊中執行,執行完成之后,再取出第二個微任務,直到微任務訊息佇列為空,然后去宏任務訊息佇列中取第一個宏任務推入堆疊中執行,當該宏任務執行完畢之后,在下一個宏任務執行前,再依次取出微任務訊息佇列中的所有微任務入堆疊執行,
6、上述程序不斷回圈,每當微任務佇列清空,可作為本輪事件回圈的結束,
有幾個關鍵點如下:
1、所有微任務總會在下一個宏任務之前全部執行完畢,宏任務必然是在微任務之后才執行的(因為微任務實際上是宏任務的其中一個步驟),
2、宏任務按順序執行,且瀏覽器在每個宏任務之間渲染頁面
3、所有微任務也按順序執行,且在以下場景會立即執行所有微任務

  • 每個回呼之后且js執行堆疊中為空,
  • 每個宏任務結束后,

我們通過幾個示例來加深一下理解:

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)

流程如下:
1、整體script作為第一個宏任務進入主執行緒,遇到setTimeout入堆疊處理,發現是異步函式(宏任務),出堆疊,移交給Web API處理,0秒等待后,將回呼函式加到宏任務佇列尾部,
2、遇到new Promise,入堆疊處理,發現是同步任務,直接執行,console輸出1;
3、遇到then,入堆疊處理,發現是異步函式(微任務),出堆疊,移交給Web API處理,將回呼函式加入微任務佇列尾部;
4、遇到console.log(2),入堆疊處理,同步任務,直接console輸出2,出堆疊,
5、堆疊已清空,檢查微任務佇列,
6、取出第一個回呼函式,入堆疊處理,發現是同步任務,直接console輸出3,出堆疊;
7、繼續從微任務佇列中取出下一個微任務,發現微任務佇列已經清空,結束第一輪事件回圈,
8、從宏任務佇列中取出第一個宏任務,入堆疊處理,發現是同步任務,直接console輸出4,
所以,最終輸出結果為:1、2、3、4
我們先稍微改變一下:

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
  Promise.resolve().then(_ => {
    console.log('before timeout')
  }).then(_ => {
    Promise.resolve().then(_ => {
      console.log('also before timeout')
    })
  })
})

console.log(2)

最終輸出結果為:1 > 2 > 3 > before timeout > also before timeout > 4,

before timeout與also before timeout在4之前輸出的原因是,在微任務執行的程序中,新產生的微任務會被直接添加到微任務佇列尾部,并在下一宏任務執行之前,全部執行掉,
而如果在微任務執行的程序中,新產生了宏任務,則會進入到宏任務佇列尾部,按照宏任務順序在后面的事件回圈中執行,
再來看一個嵌套的示例:

Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})

setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0)

最后輸出結果是Promise1 > setTimeout1 > Promise2 > setTimeout2

1、一開始執行堆疊的同步任務執行完畢,會去microtasks queues中找,清空microtasks queues,輸出Promise1,同時會生成一個異步任務setTimeout1,
2、去宏任務佇列查看此時佇列時setTimeout1在setTimeout2之前,因為setTimeout1執行堆疊一開始的時候就開始異步執行,所以輸出setTimeout1,
3、在執行setTimeout1時會生成Promise2的一個microtasks,放入 microtasks queues 中,接著又是一個回圈,去清空 microtasks queues ,輸出 Promise2
4、清空完 microtasks queues ,就又會去宏任務佇列取一個,這回取的是 setTimeout2

最后來一個復雜的示例,檢測一下是否真正掌握了事件回圈的機制,

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

最終的輸出結果為1,7,6,8,2,4,3,5,9,11,10,12,

setTimeout和setTimeInterval

setTimeout()

setTimeout函式用來指定某個函式或者某個段代碼,在多少毫秒之后執行,它回傳一個整數,表示定時器的編號,以后可以用來取消這個定時器,

var timerId = setTimeout(func|code, delay);

上面代碼中,setTimeout函式接受兩個引數,第一個引數func|code是將要推遲執行的函式名或者一段代碼,第二個引數delay是推遲執行的毫秒數,

console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);
// 1
// 3
// 2

上面代碼會先輸出1和3,然后等待1000毫秒再輸出2,注意,console.log(2)必須以字串的形式,作為setTimeout的引數,
如果推遲執行的是函式,就直接將函式名,作為setTimeout的引數,

function f() {
  console.log(2);
}

setTimeout(f, 1000);

setTimeout的第二個引數如果省略,則默認為0,

setTimeout(f)
// 等同于
setTimeout(f, 0)

還有一個需要注意的地方,如果回呼函式是物件的方法,那么setTimeout使得方法內部的this關鍵字指向全域環境,而不是定義時所在的那個物件,

var x = 1;

var obj = {
  x: 2,
  y: function () {
    console.log(this.x);
  }
};

setTimeout(obj.y, 1000) // 1

為了防止出現這個問題,一種解決方法是將obj.y放入一個函式,

var x = 1;

var obj = {
  x: 2,
  y: function () {
    console.log(this.x);
  }
};

setTimeout(function () {
  obj.y();
}, 1000);
// 2

上面代碼中,obj.y放在一個匿名函式之中,這使得obj.y在obj的作用域執行,而不是在全域作用域內執行,所以能夠顯示正確的值,

另一種解決方法是,使用bind方法,將obj.y這個方法系結在obj上面,

var x = 1;

var obj = {
  x: 2,
  y: function () {
    console.log(this.x);
  }
};

setTimeout(obj.y.bind(obj), 1000)

setInterval()

setInterval指定某個任務每隔一段時間就執行一次,也就是無限次的定時執行,

var i = 1
var timer = setInterval(function() {
  console.log(2);
}, 1000)

上面代碼中,每隔1000毫秒就輸出一個2,會無限運行下去,直到關閉當前視窗,

clearTimeout(),clearInterval()

setTimeoutsetInterval函式,都回傳一個整數值,表示計數器編號,將該整數傳入clearTimeoutclearInterval函式,就可以取消對應的定時器,

var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);

clearTimeout(id1);
clearInterval(id2);

setTimeout和setInterval回傳的整數值是連續的,也就是說,第二個setTimeout方法回傳的整數值,將比第一個的整數值大1,
利用這一點,可以寫一個函式,取消當前所有的setTimeout定時器,

(function() {
  // 每輪事件回圈檢查一次
  var gid = setInterval(clearAllTimeouts, 0);

  function clearAllTimeouts() {
    var id = setTimeout(function() {}, 0);
    while (id > 0) {
      if (id !== gid) {
        clearTimeout(id);
      }
      id--;
    }
  }
})();

上面代碼中,先呼叫setTimeout,得到一個計算器編號,然后把編號比它小的計數器全部取消,

debounce函式

防抖動函式,
有時,我們不希望回呼函式被頻繁呼叫,比如,用戶填入網頁輸入框的內容,希望通過 Ajax 方法傳回服務器,jQuery 的寫法如下,

$('textarea').on('keydown', ajaxAction);

這樣寫有一個很大的缺點,就是如果用戶連續擊鍵,就會連續觸發keydown事件,造成大量的 Ajax 通信,
正確的做法應該是,設定一個門檻值,表示兩次 Ajax 通信的最小間隔時間,如果在間隔時間內,發生新的keydown事件,則不觸發 Ajax 通信,并且重新開始計時,如果過了指定時間,沒有發生新的keydown事件,再將資料發送出去,
這種做法叫做 debounce(防抖動),假定兩次 Ajax 通信的間隔不得小于2500毫秒,上面的代碼可以改寫成下面這樣,

$('textarea').on('keydown', debounce(ajaxAction, 2500));

function debounce(fn, delay){
  var timer = null; // 宣告計時器
  return function() {
    var context = this;
    var args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      fn.apply(context, args);
    }, delay);
  };
}

上面代碼中,只要在2500毫秒之內,用戶再次擊鍵,就會取消上一次的定時器,然后再新建一個定時器,這樣就保證了回呼函式之間的呼叫間隔,至少是2500毫秒,

運行機制

setTimeout和setInterval的運行機制,是將指定的代碼移出本輪事件回圈,等到下一輪事件回圈,再檢查是否到了指定時間,如果到了,就執行對應的代碼;如果不到,就繼續等待,
這意味著,setTimeout和setInterval指定的回呼函式,必須等到本輪事件回圈的所有同步任務都執行完,才會開始執行,由于前面的任務到底需要多少時間執行完,是不確定的,所以沒有辦法保證,setTimeout和setInterval指定的任務,一定會按照預定時間執行,

setTimeout(someTask, 100);
veryLongTask();

上面代碼的setTimeout,指定100毫秒以后運行一個任務,但是,如果后面的veryLongTask函式(同步任務)運行時間非常長,過了100毫秒還無法結束,那么被推遲運行的someTask就只有等著,等到veryLongTask運行結束,才輪到它執行,
再看一個setInterval的例子,

setInterval(function () {
  console.log(2);
}, 1000);

sleep(3000);

function sleep(ms) {
  var start = Date.now();
  while ((Date.now() - start) < ms) {
  }
}

上面代碼中,setInterval要求每隔1000毫秒,就輸出一個2,但是,緊接著的sleep陳述句需要3000毫秒才能完成,那么setInterval就必須推遲到3000毫秒之后才開始生效,注意,生效后setInterval不會產生累積效應,即不會一下子輸出三個2,而是只會輸出一個2,

setTimeout(f, 0)

含義

setTimeout的作用是將代碼推遲到指定時間執行,如果指定時間為0,即setTimeout(f, 0),setTimeout(f, 0)會在下一輪事件回圈一開始就執行,,必須要等到當前腳本的同步任務,全部處理完以后,才會執行setTimeout指定的回呼函式f,

setTimeout(function () {
  console.log(1);
}, 0);
console.log(2);

上面代碼先輸出2,再輸出1,因為2是同步任務,在本輪事件回圈執行,而1是下一輪事件回圈執行,
總之,setTimeout(f, 0)這種寫法的目的是,盡可能早地執行f,但是并不能保證立刻就執行f,
實際上,setTimeout(f, 0)不會真的在0毫秒之后運行,不同的瀏覽器有不同的實作,以 Edge 瀏覽器為例,會等到4毫秒之后運行,如果電腦正在使用電池供電,會等到16毫秒之后運行;如果網頁不在當前 Tab 頁,會推遲到1000毫秒(1秒)之后運行,這樣是為了節省系統資源,

應用

setTimeout(f, 0)有幾個非常重要的用途,它的一大應用是,可以調整事件的發生順序,比如,網頁開發中,某個事件先發生在子元素,然后冒泡到父元素,即子元素的事件回呼函式,會早于父元素的事件回呼函式觸發,如果,想讓父元素的事件回呼函式先發生,就要用到setTimeout(f, 0),

// HTML 代碼如下
// <input type="button" id="myButton" value="click">

var input = document.getElementById('myButton');

input.onclick = function A() {
  setTimeout(function B() {
    input.value +=' input';
  }, 0)
};

document.body.onclick = function C() {
  input.value += ' body'
};

上面代碼在點擊按鈕后,先觸發回呼函式A,然后觸發函式C,函式A中,setTimeout將函式B推遲到下一輪事件回圈執行,這樣就起到了,先觸發父元素的回呼函式C的目的了,

另一個應用是,用戶自定義的回呼函式,通常在瀏覽器的默認動作之前觸發,比如,用戶在輸入框輸入文本,keypress事件會在瀏覽器接收文本之前觸發,因此,下面的回呼函式是達不到目的的,

// HTML 代碼如下
// <input type="text" id="input-box">

document.getElementById('input-box').onkeypress = function (event) {
  this.value = this.value.toUpperCase();
}

上面代碼想在用戶每次輸入文本后,立即將字符轉為大寫,但是實際上,它只能將本次輸入前的字符轉為大寫,因為瀏覽器此時還沒接收到新的文本,所以this.value取不到最新輸入的那個字符,只有用setTimeout改寫,上面的代碼才能發揮作用,

document.getElementById('input-box').onkeypress = function() {
  var self = this;
  setTimeout(function() {
    self.value = self.value.toUpperCase();
  }, 0);
}

上面代碼將代碼放入setTimeout之中,就能使得它在瀏覽器接收到文本之后觸發,

由于setTimeout(f, 0)實際上意味著,將任務放到瀏覽器最早可得的空閑時段執行,所以那些計算量大、耗時長的任務,常常會被放到幾個小部分,分別放到setTimeout(f, 0)里面執行,

var div = document.getElementsByTagName('div')[0];

// 寫法一
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
  div.style.backgroundColor = '#' + i.toString(16);
}

// 寫法二
var timer;
var i=0x100000;

function func() {
  timer = setTimeout(func, 0);
  div.style.backgroundColor = '#' + i.toString(16);
  if (i++ == 0xFFFFFF) clearTimeout(timer);
}

timer = setTimeout(func, 0);

上面代碼有兩種寫法,都是改變一個網頁元素的背景色,寫法一會造成瀏覽器“堵塞”,因為 JavaScript 執行速度遠高于 DOM,會造成大量 DOM 操作“堆積”,而寫法二就不會,這就是setTimeout(f, 0)的好處,

另一個使用這種技巧的例子是代碼高亮的處理,如果代碼塊很大,一次性處理,可能會對性能造成很大的壓力,那么將其分成一個個小塊,一次處理一塊,比如寫成setTimeout(highlightNext, 50)的樣子,性能壓力就會減輕,

物件的方法

Object.create

生成實體物件的常用方法是,使用new命令讓建構式回傳一個實體,但是很多時候,只能拿到一個實體物件,那么能不能從一個實體物件,生成另一個實體物件呢?

JavaScript 提供了Object.create()方法,用來滿足從一個實體物件生成另外一個實體物件,該方法接受一個物件作為引數,然后以它為原型,回傳一個實體物件,該實體完全繼承原型物件的屬性和方法,

function A(){
    this.test = function() { //實體物件的方法
        console.log('test')
    }
}
A.prototype.print = function() { //原型物件的方法
    console.log('print')
}

var a = new A()

var b = Object.create(a)
b.test() //test
b.print() //print

上面代碼中,Object.create()方法以A物件為原型,生成了B物件,B繼承了A的所有屬性和方法,
實際上,Object.create()方法可以用下面的代碼代替,

if (typeof Object.create !== 'function') {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

上面代碼表明,Object.create()方法的實質是新建一個空的建構式F,然后讓F.prototype屬性指向引數物件obj,最后回傳一個F的實體,從而實作讓該實體繼承obj的屬性,
如果想要生成一個不繼承任何屬性(比如沒有toString()和valueOf()方法)的物件,可以將Object.create()的引數設為null

var obj = Object.create(null);

obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'

Object.create()方法生成的新物件,動態繼承了原型,在原型上添加或修改任何方法,會立刻反映在新物件之上,

var obj1 = { p: 1 };
var obj2 = Object.create(obj1);

obj1.p = 2;
obj2.p // 2

Object.create()方法生成的物件,繼承了它的原型物件的建構式

function A() {}
var a = new A();
var b = Object.create(a);

b.constructor === A // true
b instanceof A // true
let a = {
    name:'lichangan',
    age:18,
}

let b = {
    name:'xiaowu',
    book: {
        title:'js',
        price:50
    }
}

let c = Object.create(b)
console.log(c.book.price) //50
b.book.price = 18
console.log(c.book.price) //18

Object.assign

將多個源物件的屬性復制到一個目標物件中,如果有相同屬性,源物件會覆寫目標物件,

const source1 = {
    a:10,
    b:20
}

const target = {
    a:40,
    c:80
}

Object.assign(target,source1)
console.log(target.a) //10 
console.log(target.b) // 20 

const Source2 = {
	d:d
}
Object.assign(target,source1,source2)

Object.assign()的拷貝行為是一個淺拷貝行為,

let a = {
    name:'lichangan',
    age:18,
}

let b = {
    name:'xiaowu',
    book: {
        title:'js',
        price:50
    }
}
let c = {}
Object.assign(c,a,b)
console.log(c)

a.age = 19;
b.book.price = 40
console.log(c.age) //18
console.log(c.book.price) // 40

JavaScript 不提供多重繼承功能,即不允許一個物件同時繼承多個物件,但是,可以通過變通方法,實作這個功能,

function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
  //this.hello和this.world都是實體物件的屬性,不是原型物件的屬性
  //通過顯示呼叫M1和M2的建構式來系結到當前this上面,
  M1.call(this); 
  M2.call(this);
}

// 繼承 M1,根據實體創建一個物件,
S.prototype = Object.create(M1.prototype);
//S.prototype = M1.prototype 這種寫法是參考,當我們修改S的原型物件時,M1也會受到影響,
// 繼承鏈上加入 M2
Object.assign(S.prototype, M2.prototype);

// 指定建構式
S.prototype.constructor = S;

var s = new S();
s.hello // 'hello'
s.world // 'world'

上面代碼中,子類S同時繼承了父類M1和M2,這種模式又稱為 Mixin(混入),

Object.is

Object.is():是ES6新增的用來比較兩個值是否嚴格相等的方法,與===的行為基本一致,

==:比較運算子,兩邊值型別不同的時候,先進行型別轉換,再比較;
===:嚴格比較運算子,不做型別轉換,型別不同就是不等;

而在ES6中,Object.is()類似于===,但在三等號判等的基礎上特別處理了 NaN-0+0 ,保證 -0+0 不再相同,但 Object.is(NaN, NaN) 會回傳 true,

下面這些情況Object.is()會認為兩個值是相同的:

兩個值都是 undefined
兩個值都是 null
兩個值都是 true 或者都是 false
兩個值是由相同個數的字符按照相同的順序組成的字串
兩個值指向同一個物件
兩個值都是數字并且
都是正零 +0
都是負零 -0
都是 NaN
都是除零和 NaN 外的其它同一個數字

Object.defineProperty

Object.defineProperty()方法會直接在一個物件上定義一個新屬性或者修改一個物件的現有屬性,并且回傳此物件,可以用來監視某個物件的讀寫,

 //模擬Vue中的data選項
      let data = {
        msg: "hello",
      };
      //模擬Vue實體
      let vm = {};

      //資料劫持:當訪問或者設定vm中的成員的時候,做一些干預操作,
      //給vm物件新建一個屬性msg,當訪問或者設定vm中的msg的時候,做一些干預操作,
      Object.defineProperty(vm, "msg", {
        //可列舉(可遍歷)
        enumerable: true,
        //可配置(可以使用delete洗掉,可以通過defineProperty重新定義)
        configurable: true,
        //當獲取值的時候執行
        get() {
          console.log("get:" + data.msg);
          return data.msg;
        },
        //當設定值的時候執行
        set(newValue) {
          console.log("set:" + newValue);
          if (newValue === data.msg) {
            return;
          }
          data.msg = newValue;
          //資料修改,更新DOM值
          document.querySelector("#app").textContent = data.msg;
        },
      });
      
      vm.msg = "HelloWorld!"

Object.prototype.isPrototypeOf()

實體物件的isPrototypeOf方法,用來判斷該物件是否為引數物件的原型

var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);

o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true

Object.prototype.proto

實體物件的__proto__屬性(前后各兩個下劃線),回傳該物件的原型,該屬性可讀寫,

var obj = {};
var p = {};

obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true

根據語言標準,__proto__屬性只有瀏覽器才需要部署,其他環境可以沒有這個屬性,它前后的兩根下劃線,表明它本質是一個內部屬性,不應該對使用者暴露,因此,應該盡量少用這個屬性,而是用Object.getPrototypeOf()和Object.setPrototypeOf(),進行原型物件的讀寫操作,

Proxy

Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫,
基本用法:

let proxy = new Proxy(target,handler);

target引數表示要攔截的目標物件
handle引數也是一個物件,用來制定攔截行為,
Proxy支持13中攔截行為,這里只用到兩種,

get(target,propKey,receiver);

用來攔截某個屬性的讀取操作,可以接受三個引數:
target:目標物件
propKey:屬性名
receiver(可選):proxy實體本身

set(target,propKey,value,receiver)

用于攔截某個屬性的賦值操作,可以接受四個引數:
target:目標物件
propKey:屬性名
value:屬性值
receiver(可選):Proxy實體本身,

  //模擬Vue中的data選項
      let data = {
        msg: "hello",
        count:10,
      };
      //模擬Vue實體
      let vm = new Proxy(data,{
          //當訪問vm的成員會執行
          get(target,key) {
              console.log("get,key:",key,target[key])
              return target[key]
          },
          //當設定vm的成員時會執行
          set(target,key,newValue){
              console.log('set,key:',key,newValue)
              if(target[key] === newValue){
                  return
              }
              target[key] = newValue
              document.querySelector('#app').textContent = target[key]
          }
      })
    
      vm.msg = "HelloWorld!"

Proxy與Object.defineProperty

(1)
Object.defineProperty()只能監視屬性的讀寫,
Object.defineProperty無法監聽陣列變化,

Proxy監聽的是整個物件,
Proxy可以監視陣列更加方便,
Proxy有多達13種攔截方法,

const listPropery = new Proxy(list,{
    set(target,property,value){
        target[property] = value
        return true
    }
})
listPropery.push(100)

面向物件

this指標

使用場合

在普通的函式中,包含this的函式,誰呼叫了它,它就指向誰
也就是說,包含 this 的函式 只在乎是誰呼叫了它,跟在哪里進行的函式宣告沒有關系,
箭頭函式體內的this物件,就是定義該函式時所在的作用域指向的物件,而不是使用時所在的作用域指向的物件,

(1)全域環境

全域環境使用this,它指的就是頂層物件window

this === window // true

(2)函式內部

非嚴格模式下,this默認指向全域物件window
嚴格模式,thisundefined

function test(){
    var a = 1;
    console.log(this.a);
}

test(); //undefined

(3)建構式

建構式中的this,指的是實體物件,

var Obj = function (p) {
  this.p = p;
};

上面代碼定義了一個建構式Obj,由于this指向實體物件,所以在建構式內部定義this.p,就相當于定義實體物件有一個p屬性,

(4)物件的方法

如果物件的方法里面包含thisthis的指向就是方法運行時所在的物件,該方法賦值給另一個物件,就會改變this的指向,

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};
console.log(o.f());  //37
//this指向呼叫方法的實體物件o
var a = o.f;
console.log(a()):  //undefined
//var a實際上是window.a,把o.f賦值給window.a,
//a()實際上是window.a(),a方法內部的指標指向呼叫它的window物件,window物件沒有prop屬性,所以undefined,

原型鏈中的this

原型鏈中的this仍然指向呼叫它的物件,

setTimeout和setInterval

對于延時函式內部的回呼函式的this指向全域物件window(當然我們可以通過bind方法改變其內部函式的this指向)

function Person() {  
    this.age = 0;  
    setTimeout(function() {
        console.log(this); //列印window物件
    }, 3000);
}

var p = new Person();//3秒后回傳 window 物件
//立即執行函式通過bind系結
function Person() {  
    this.age = 0;  
    setTimeout((function() {
        console.log(this);
    }).bind(this), 3000);
}

箭頭函式中的this

箭頭函式體內的this物件,就是定義該函式時所在的作用域指向的物件,而不是使用時所在的作用域指向的物件,

下面是普通函式的列子:

var name = 'window'; // 其實是window.name = 'window'

var A = {
   name: 'A',
   sayHello: function(){
      console.log(this.name)
   }
}

A.sayHello();// 輸出A

var B = {
  name: 'B'
}

A.sayHello.call(B);//輸出B

A.sayHello.call();//不傳引數指向全域window物件,輸出window.name也就是window

改造一下:

var name = 'window'; 
var A = {
   name: 'A',
   sayHello: () => {
      console.log(this.name)
   }
}

A.sayHello();// 輸出的是window

這里的箭頭函式,也就是sayHello(),所在的作用域其實是最外層的js環境,因為沒有其他函式包裹;然后最外層的js環境指向的物件是winodw物件,所以這里的this指向的是window物件,

繼續改造一下:

var name = 'window'; 

var A = {
   name: 'A',
   sayHello: function(){
      var s = () => console.log(this.name)
      return s//回傳箭頭函式s
   }
}

var sayHello = A.sayHello();
sayHello();// 輸出A 

var B = {
   name: 'B';
}

sayHello.call(B); //還是A
sayHello.call(); //還是A

這樣就做到了永遠指向A物件了

function Person() {  
    this.age = 0;  
    setInterval(() => {
        // 回呼里面的 `this` 變數就指向了期望的那個物件了
        this.age++;
    }, 3000);
}

DOM事件處理函式

當函式被當做監聽事件處理函式時, 其 this 指向觸發該事件的元素 (針對于addEventListener事件),

行內事件

行內事件中的this指向分兩種情況:
(1)當代碼被行內處理函式呼叫時,它的this指向監聽器所在的DOM元素
(2)當代碼被包括在函式內部執行時,其this指向等同于 函式直接呼叫的情況,即在非嚴格模式指向全域物件window, 在嚴格模式指向undefined

系結this的方法

call,apply,bind三者的區別

  • 三者都是用來改變this指向,
  • 三者第一個引數都是this要指向的物件,如果如果沒有這個引數或引數為undefinednull,則默認指向全域window
  • callapply改變this指向后會立即執行,bind改變this指向后不會立即執行,而是回傳一個新的函式,
  • 三者都可以傳參,call是傳入引數串列,apply是傳入陣列,且callapply是一次性傳入引數,bind也是通過引數串列傳參,而bind可以分為多次傳入,第一次在使用bind系結時傳入引數,如果需要可以在呼叫新的函式的時候再次傳入引數,

Function.prototype.call()

函式實體的call方法,可以指定函式內部this的指向(即函式執行時所在的作用域),然后在所指定的作用域中,呼叫該函式,

call方法的引數,應該是一個物件,如果引數為空、nullundefined,則默認傳入全域物件,

var n = 123;
var obj = { n: 456 };

function print() {
  console.log(this.n);
}

print.call() // 123
print.call(null) // 123
print.call(undefined) // 123
print.call(window) // 123
print.call(obj) // 456

call方法還可以接受多個引數,

func.call(thisValue, arg1, arg2, ...)

call的第一個引數就是this所要指向的那個物件,后面的引數則是函式呼叫時所需的引數,

function add(a, b) {
  return a + b;
}

add.call(this, 1, 2) // 3
  • 應用場景: 呼叫物件的原生方法,
var obj = {};
obj.hasOwnProperty('toString') // false

// 覆寫掉繼承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

上面代碼中,hasOwnProperty是obj物件繼承的方法,如果這個方法一旦被覆寫,就不會得到正確結果,call方法可以解決這個問題,它將hasOwnProperty方法的原始定義放到obj物件上執行,這樣無論obj上有沒有同名方法,都不會影響結果,

Function.prototype.apply()

apply方法的作用與call方法類似,也是改變this指向,然后再呼叫該函式,唯一的區別就是,它接收一個陣列作為函式執行時的引數,使用格式如下,

func.apply(thisValue, [arg1, arg2, ...])

apply方法的第一個引數也是this所要指向的那個物件,如果設為nullundefined,則等同于指定全域物件,第二個引數則是一個陣列,該陣列的所有成員依次作為引數,傳入原函式,原函式的引數,在call方法中必須一個個添加,但是在apply方法中,必須以陣列形式添加

function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
  • 一些有趣的應用
    (1)找出陣列的最大元素
    JavaScript 不提供找出陣列最大元素的函式,結合使用apply方法和Math.max方法,就可以回傳陣列的最大元素,
Math.max(1,2,3,4,5) //本來需要傳入的是引數串列
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15 借助apply的特性傳入陣列,實作陣列的求最大值,

(2)將陣列的空元素變為undefined
通過apply方法,利用Array建構式將陣列的空元素變成undefined

Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]

空元素與undefined的差別在于,陣列的forEach方法會跳過空元素,但是不會跳過undefined,因此,遍歷內部元素的時候,會得到不同的結果,

(3) 轉換類似陣列的物件
另外,利用陣列物件的slice方法,可以將一個類似陣列的物件(比如arguments物件)轉為真正的陣列,

Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]

上面代碼的apply方法的引數都是物件,但是回傳結果都是陣列,這就起到了將物件轉成陣列的目的,從上面代碼可以看到,這個方法起作用的前提是,被處理的物件必須有length屬性,以及相對應的數字鍵,

(4) 系結回呼函式的物件

var o = new Object();

o.f = function () {
  console.log(this === o);
}

var f = function (){
  o.f.apply(o);
  // 或者 o.f.call(o);
};

// jQuery 的寫法
$('#button').on('click', f);

上面代碼中,點擊按鈕以后,控制臺將會顯示true,由于apply()方法(或者call()方法)不僅系結函式執行時所在的物件,還會立即執行函式,因此不得不把系結陳述句寫在一個函式體內,更簡潔的寫法是采用下面介紹的bind()方法,

Function.prototype.bind()

bind()方法用于將函式體內的this系結到某個物件,然后回傳一個新函式,

var d = new Date();
d.getTime() // 1481869925657

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

上面代碼中,我們將d.getTime()方法賦給變數print,然后呼叫print()就報錯了,這是因為getTime()方法內部的this,系結Date物件的實體,賦給變數print以后,內部的this已經不指向Date物件的實體了,

bind()方法可以解決這個問題,

var print = d.getTime.bind(d);
print() // 1481869925657

上面代碼中,bind()方法將getTime()方法內部的this系結到d物件,這時就可以安全地將這個方法賦值給其他變數了,

bind()還可以接受更多的引數,將這些引數系結原函式的引數,

function add(x, y) {
  return x + y;
}

var plus5 = add.bind(null, 5);
plus5(10) // 15

上面代碼中,函式add()內部并沒有this,使用bind()方法的主要目的是系結引數x,以后每次運行新函式plus5(),就只需要提供另一個引數y就夠了,而且因為add()內部沒有this,所以bind()的第一個引數是null,不過這里如果是其他物件,也沒有影響,

  • 使用注意點
    (1)每一次回傳一個新函式
    bind()方法每運行一次,就回傳一個新函式,這會產生一些問題,比如,監聽事件的時候,不能寫成下面這樣,
element.addEventListener('click', o.m.bind(o));

上面代碼中,click事件系結bind()方法生成的一個匿名函式,這樣會導致無法取消系結,所以下面的代碼是無效的,

element.removeEventListener('click', o.m.bind(o));

正確的方法是寫成下面這樣:

var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);

(2) 結合回呼函式使用
**回呼函式是 JavaScript 最常用的模式之一,但是一個常見的錯誤是,將包含this的方法直接當作回呼函式,**解決方法就是使用bind()方法,將counter.inc()系結counter

var counter = {
  count: 0,
  inc: function () {
    'use strict';
    this.count++;
  }
};

function callIt(callback) {
  callback();
}

callIt(counter.inc.bind(counter));
counter.count // 1

上面代碼中,callIt()方法會呼叫回呼函式,這時如果直接把counter.inc傳入,呼叫時counter.inc()內部的this就會指向全域物件,使用bind()方法將counter.inc系結counter以后,就不會有這個問題,this總是指向counter

還有一種情況比較隱蔽,就是某些陣列方法可以接受一個函式當作引數,這些函式內部的this指向,很可能也會出錯,

var obj = {
  name: '張三',
  times: [1, 2, 3],
  print: function () {
    this.times.forEach(function (n) {
      console.log(this.name);
    });
  }
};

obj.print()
// 沒有任何輸出

上面代碼中,obj.print內部this.times的this是指向obj的,這個沒有問題,但是,forEach()方法的回呼函式內部的this.name卻是指向全域物件,導致沒有辦法取到值,稍微改動一下,就可以看得更清楚,

obj.print = function () {
  this.times.forEach(function (n) {
    console.log(this === window);
  });
};

obj.print()
// true
// true
// true

解決這個問題,也是通過bind()方法系結this

obj.print = function () {
  this.times.forEach(function (n) {
    console.log(this.name);
  }.bind(this));
};

obj.print()
// 張三
// 張三
// 張三

(3)結合call()方法使用
利用bind()方法,可以改寫一些 JavaScript 原生方法的使用形式,以陣列的slice()方法為例,

[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

上面的代碼中,陣列的slice方法從[1, 2, 3]里面,按照指定的開始位置和結束位置,切分出另一個陣列,這樣做的本質是在[1, 2, 3]上面呼叫Array.prototype.slice()方法,因此可以用call方法表達這個程序,得到同樣的結果,

call()方法實質上是呼叫Function.prototype.call()方法,因此上面的運算式可以用bind()方法改寫,

var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

上面代碼的含義就是,將Array.prototype.slice變成Function.prototype.call方法所在的物件,呼叫時就變成了Array.prototype.slice.call,類似的寫法還可以用于其他陣列方法,

var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);

var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]

pop(a)
a // [1, 2, 3]

如果再進一步,將Function.prototype.call方法系結到Function.prototype.bind物件,就意味著bind的呼叫形式也可以被改寫,

function f() {
  console.log(this.v);
}

var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123

上面代碼的含義就是,將Function.prototype.bind方法系結在Function.prototype.call上面,所以bind方法就可以直接使用,不需要在函式實體上使用,

物件的繼承

原型物件概述

建構式的缺點

JavaScript 通過建構式生成新物件,因此建構式可以視為物件的模板,實體物件的屬性和方法,可以定義在建構式內部,

同一個建構式的多個實體之間,無法共享屬性,從而造成對系統資源的浪費,

function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.miao = function () {
    console.log('喵喵');
  };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.miao === cat2.miao// false
//這兩個方法的內容地址不一樣,都是各自獨立的記憶體地址,

面代碼中,cat1和cat2是同一個建構式的兩個實體,它們都具有miao方法,由于miao方法是生成在每個實體物件上面,所以兩個實體就生成了兩次,也就是說,每新建一個實體,就會新建一個miao方法,這既沒有必要,又浪費系統資源,因為所有miao方法都是同樣的行為,完全應該共享,

prototype 屬性的作用

JavaScript 繼承機制的設計思想就是,原型物件的所有屬性和方法,都能被實體物件共享,也就是說,如果屬性和方法定義在原型上,那么所有實體物件就能共享,不僅節省了記憶體,還體現了實體物件之間的聯系,

JavaScript 規定,每個函式都有一個prototype屬性,指向一個物件,這個物件就是原型物件,

function Animal(name) {
  this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

原型物件的屬性不是實體物件自身的屬性,只要修改原型物件,變動就立刻會體現在所有實體物件上,

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

上面代碼中,原型物件的color屬性的值變為yellow,兩個實體物件的color屬性立刻跟著變了,這是因為實體物件其實沒有color屬性,都是讀取原型物件的color屬性,

當實體物件本身沒有某個屬性或方法的時候,它會到原型物件去尋找該屬性或方法,這就是原型物件的特殊之處,
如果實體物件自身就有某個屬性或方法,它就不會再去原型物件尋找這個屬性或方法,

原型鏈

JavaScript 規定,所有物件都有自己的原型物件(prototype),由于原型物件也是物件,所以它也有自己的原型,因此,就會形成一個“原型鏈”,
如果一層層地上溯,所有物件的原型最終都可以上溯到Object.prototype,也就是說,所有物件都繼承了Object.prototype的屬性,這就是所有物件都有valueOftoString方法的原因,因為這是從Object.prototype繼承的,
Object.prototype的原型是nullnull沒有任何屬性和方法,也沒有自己的原型,因此,原型鏈的盡頭就是null

讀取物件的某個屬性時,JavaScript 引擎先尋找物件本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找,如果直到最頂層的Object.prototype還是找不到,則回傳undefined,如果物件自身和它的原型,都定義了一個同名屬性,那么優先讀取物件自身的屬性,這叫做“覆寫”(overriding),

注意,一級級向上,在整個原型鏈上尋找某個屬性,對性能是有影響的,所尋找的屬性在越上層的原型物件,對性能的影響越大,如果尋找某個不存在的屬性,將會遍歷整個原型鏈,

constructor 屬性

prototype物件有一個constructor屬性,默認指向prototype所在物件的建構式,

function P() {}
P.prototype.constructor === P // true

由于constructor屬性定義在prototype物件上面,意味著可以被所有實體物件繼承,
constructor屬性的作用是,可以得知某個實體物件,到底是哪一個建構式產生的,
constructor屬性表示原型物件與建構式之間的關聯關系,如果修改了原型物件,一般會同時修改constructor屬性,防止參考的時候出錯,

function Person(name) {
  this.name = name;
}

Person.prototype.constructor === Person // true

Person.prototype = {
  method: function () {}
};

Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true

上面代碼中,建構式Person的原型物件改掉了,但是沒有修改constructor屬性,導致這個屬性不再指向Person,由于Person的新原型是一個普通物件,而普通物件的constructor屬性指向Object建構式,導致Person.prototype.constructor變成了Object

所以,修改原型物件時,一般要同時修改constructor屬性的指向,

// 壞的寫法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

// 好的寫法
C.prototype = {
  constructor: C,
  method1: function (...) { ... },
  // ...
};

// 更好的寫法
C.prototype.method1 = function (...) { ... };

如果不能確定constructor屬性是什么函式,還有一個辦法:通過name屬性,從實體得到建構式的名稱,

function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"

instanceof運算子

instanceof運算子回傳一個布林值,表示物件是否為某個建構式的實體

var v = new Vehicle();
v instanceof Vehicle // true

上面代碼中,物件v是建構式Vehicle的實體,所以回傳true,
instanceof運算子的左邊是實體物件,右邊是建構式,它會檢查右邊建構式的原型物件(prototype),是否在左邊物件的原型鏈上,因此,下面兩種寫法是等價的,

v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)

上面代碼中,Vehicle是物件v的建構式,它的原型物件是Vehicle.prototypeisPrototypeOf()方法是 JavaScript 提供的原生方法,用于檢查某個物件是否為另一個物件的原型,

由于instanceof檢查整個原型鏈,因此同一個實體物件,可能會對多個建構式都回傳true,

var d = new Date();
d instanceof Date // true
d instanceof Object // true

由于任意物件(除了null)都是Object的實體,所以instanceof運算子可以判斷一個值是否為非null的物件,

var obj = { foo: 123 };
obj instanceof Object // true

null instanceof Object // false

instanceof的原理是檢查右邊建構式的prototype屬性,是否在左邊物件的原型鏈上有一種特殊情況,就是左邊物件的原型鏈上,只有null物件,這時,instanceof判斷會失真,

var obj = Object.create(null);
typeof obj // "object"
obj instanceof Object // false

上面代碼中,Object.create(null)回傳一個新物件obj,它的原型是null,右邊的建構式Objectprototype屬性,不在左邊的原型鏈上,因此instanceof就認為obj不是Object的實體,這是唯一的instanceof運算子判斷會失真的情況(一個物件的原型是null),

對于undefinednullinstanceof運算子總是回傳false

利用instanceof運算子,還可以巧妙地解決,呼叫建構式時,忘了加new命令的問題,

function Fubar (foo, bar) {
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  } else {
    return new Fubar(foo, bar);
  }
}

建構式的繼承

讓一個建構式繼承另一個建構式,分兩步實作:
第一步是在子類的建構式中,呼叫父類的建構式,

function Sub(value) {
  Super.call(this);
  this.prop = value;
}

上面代碼中,Sub是子類的建構式,this是子類的實體,在實體上呼叫父類的建構式Super,就會讓子類實體具有父類實體的屬性,

第二步,是讓子類的原型指向父類的原型,這樣子類就可以繼承父類原型,

Sub.prototype = Object.create(Super.prototype);
//獲取的父類的原型物件,constructor指向父類,需要重置,
Sub.prototype.constructor = Sub; 
Sub.prototype.method = '...';

上面代碼中,Sub.prototype是子類的原型,要將它賦值為Object.create(Super.prototype),而不是直接等于Super.prototype,否則后面兩行對Sub.prototype的操作,會連父類的原型Super.prototype一起修改掉,

另外一種寫法是Sub.prototype等于一個父類實體,

Sub.prototype = new Super();

上面這種寫法也有繼承的效果,但是子類會具有父類實體的方法,有時,這可能不是我們需要的,所以不推薦使用這種寫法,

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};
// 第一步,子類繼承父類的實體
function Rectangle() {
  Shape.call(this); // 呼叫父類建構式
}
// 另一種寫法
function Rectangle() {
  this.base = Shape;
  this.base();
}

// 第二步,子類繼承父類的原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

采用這樣的寫法以后,instanceof運算子會對子類和父類的建構式,都回傳true
上面代碼中,子類是整體繼承父類,有時只需要單個方法的繼承,這時可以采用下面的寫法,

ClassB.prototype.print = function() {
  ClassA.prototype.print.call(this);
  // some code
}

多重繼承

JavaScript 不提供多重繼承功能,即不允許一個物件同時繼承多個物件,但是,可以通過變通方法,實作這個功能,

function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
 //this.hello和this.world都是實體物件的屬性,不是原型物件的屬性
  //通過顯示呼叫M1和M2的建構式來系結到當前this上面,
  M1.call(this);
  M2.call(this);
}

// 繼承 M1,根據實體創建一個物件,
S.prototype = Object.create(M1.prototype);
//S.prototype = M1.prototype 這種寫法是參考,當我們修改S的原型物件時,M1也會受到影響,
// 繼承鏈上加入 M2
Object.assign(S.prototype, M2.prototype);

// 指定建構式
S.prototype.constructor = S;

var s = new S();
s.hello // 'hello'
s.world // 'world'

上面代碼中,子類S同時繼承了父類M1和M2,這種模式又稱為 Mixin(混入),

建構式內的方法與原型上的方法的對比

  • 定義在建構式內部的方法,會在它的每一個實體上,都克隆這個方法,
  • 定義在prototype原型上的方法,會讓它的所有的實體都共享這個方法,但是不會再每個實體內部,重新定義這個方法,因為方法是共享的,所以修改方法會影響到其他的實體物件,
  • 如果我們的應用需要創建很多的物件,并且這些物件還有許多的方法,為了節省記憶體,我們建議把這些方法都定義在建構式的prototype屬性上,
    在這里插入圖片描述

ES6中的class

class的定義

JavaScript 語言中,生成實體物件的傳統方法是通過建構式,

//通過建構式定義實體物件不可共享(記憶體層面)的屬性和方法
function Point(x,y) {
    this.x = x 
    this.y = y 
}
//通過prototype定義實體物件可以共享(記憶體層面)的方法
Point.prototype.toString  = function(){
    return '(' + this.x + ',' + this.y + ')'
}

ES6中引入了class,可以把class看成是一個語法糖,

class Point {
    constructor(x,y) {
   		//定義實體屬性
        this.x = x 
        this.y = y 
    }
    //實體方法
     toString() {
        return "(" + this.x + "," + this.y + ")"
    }
}

ES 6中的類完全可以看做建構式的另一種寫法,

class Point {
}
typeof Point // "function"
Point === Point.prototype.constructor // true

上面代碼表明,類的資料型別就是函式,類本身就指向建構式,

建構式的prototype屬性,在 ES6 的“類”上面繼續存在,事實上,類的所有方法都定義在類的prototype屬性上面,

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

因此,在類的實體上面呼叫方法,其實就是呼叫原型上的方法,

由于類的方法都定義在prototype物件上面,所以類的新方法可以添加在prototype物件上面,Object.assign()方法可以很方便地一次向類添加多個方法,

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

取值函式和存值函式

與 ES5 一樣,在“類”的內部可以使用get和set關鍵字,對某個屬性設定存值函式和取值函式,攔截該屬性的存取行為,

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

靜態方法

如果在一個方法前,加上static關鍵字,就表示該方法不會被實體繼承,而是直接通過類來呼叫,這就稱為“靜態方法”,

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

注意,如果靜態方法包含this關鍵字,這個this指的是類,而不是實體,

實體屬性的新寫法

實體屬性除了可以定義在construct()方法里面的this上面,還可以定義在類的頂部,

class foo {
  bar = 'hello';
  baz = 'world';

  constructor() {
    // ...
    this.num = 10
  }
}

類的繼承

Class 可以通過extends關鍵字實作繼承,這比 ES5 的通過修改原型鏈實作繼承,要清晰和方便很多,

class Point {
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 呼叫父類的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 呼叫父類的toString()
  }
}

子類必須在constructor方法中呼叫super方法,否則新建實體時會報錯,這是因為子類自己的this物件,必須先通過父類的建構式完成塑造,得到與父類同樣的實體屬性和方法,然后再對其進行加工,加上子類自己的實體屬性和方法,如果不呼叫super方法,子類就得不到this物件,

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用來從子類上獲取父類,

Object.getPrototypeOf(ColorPoint) === Point
// true

因此,可以使用這個方法判斷,一個類是否繼承了另一個類,

super 關鍵字

super這個關鍵字,既可以當作函式使用,也可以當作物件使用,
第一種情況,super作為函式呼叫時,代表父類的建構式,ES6 要求,子類的建構式必須執行一次super函式,

class A {}

class B extends A {
  constructor() {
    super();
  }
}

第二種情況,super作為物件時,在普通方法中,指向父類的原型物件;在靜態方法中,指向父類,

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代碼中,子類B當中的super.p(),就是將super當作一個物件使用,這時,super在普通方法之中,指向A.prototype,所以super.p()就相當于A.prototype.p()

這里需要注意,由于super指向父類的原型物件,所以定義在父類實體上的方法或屬性,是無法通過super呼叫的,

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代碼中,p是父類A實體的屬性,super.p就參考不到它,
如果屬性定義在父類的原型物件上,super就可以取到,

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

上面代碼中,屬性x是定義在A.prototype上面的,所以super.x可以取到它的值,

ES6 規定,在子類普通方法中通過super呼叫父類的方法時,方法內部的this指向當前的子類實體,

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

上面代碼中,super.print()雖然呼叫的是A.prototype.print(),但是A.prototype.print()內部的this指向子類B的實體,導致輸出的是2,而不是1,也就是說,實際上執行的是super.print.call(this)

由于this指向子類實體,所以如果通過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類實體的屬性,

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

上面代碼中,super.x賦值為3,這時等同于對this.x賦值為3,而當讀取super.x的時候,讀的是A.prototype.x,所以回傳undefined

如果super作為物件,用在靜態方法之中,這時super將指向父類,而不是父類的原型物件,

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

上面代碼中,super在靜態方法之中指向父類,在普通方法之中指向父類的原型物件,

另外,在子類的靜態方法中通過super呼叫父類的方法時,方法內部的this指向當前的子類,而不是子類的實體,

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}

B.x = 3;
B.m() // 3

上面代碼中,靜態方法B.m里面,super.print指向父類的靜態方法,這個方法里面的this指向的是B,而不是B的實體,

Module模塊

Module 的加載實作

瀏覽器加載

HTML 網頁中,瀏覽器通過<script>標簽加載 JavaScript 腳本,
默認情況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標簽就會停下來,等到執行完腳本,再繼續向下渲染,如果是外部腳本,還必須加入腳本下載的時間,

如果腳本體積很大,下載和執行的時間就會很長,因此造成瀏覽器堵塞,用戶會感覺到瀏覽器“卡死”了,沒有任何回應,這顯然是很不好的體驗,所以瀏覽器允許腳本異步加載,下面就是兩種異步加載的語法,

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

上面代碼中,<script>標簽打開defer或async屬性,腳本就會異步加載,渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行后面的命令,

defer與async的區別是:defer要等到整個頁面在記憶體中正常渲染結束(DOM 結構完全生成,以及其他腳本執行完成),才會執行;async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本以后,再繼續渲染,另外,如果有多個defer腳本,會按照它們在頁面出現的順序加載,而多個async腳本是不能保證加載順序的,

加載規則

瀏覽器加載 ES6 模塊,也使用<script>標簽,但是要加入type="module"屬性,

<script type="module" src="./foo.js"></script>

瀏覽器對于帶有type="module"<script>,都是異步加載,不會造成堵塞瀏覽器,即等到整個頁面渲染完,再執行模塊腳本,等同于打開了<script>標簽的defer屬性,

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

如果網頁有多個<script type="module">,它們會按照在頁面出現的順序依次執行,
<script>標簽的async屬性也可以打開,這時只要加載完成,渲染引擎就會中斷渲染立即執行,執行完成后,再恢復渲染,

<script type="module" src="./foo.js" async></script>

一旦使用了async屬性,<script type="module">就不會按照在頁面出現的順序執行,而是只要該模塊加載完成,就執行該模塊,

ES6 模塊與Common.js模塊的差異

它們有三個重大差異,

  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的參考,
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出介面,
  • CommonJS 模塊的require()是同步加載模塊,ES6 模塊的import命令是異步加載,有一個獨立的模塊依賴的決議階段,

第二個差異是因為 CommonJS 加載的是一個物件(即module.exports屬性),該物件只有在腳本運行完才會生成,而 ES6 模塊不是物件,它的對外介面只是一種靜態定義,在代碼靜態決議階段就會生成,

CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值,

ES6 模塊的運行機制與 CommonJS 不一樣,JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀參考,等到腳本真正執行時,再根據這個只讀參考,到被加載的那個模塊里面去取值,因此,ES6 模塊是動態參考,并且不會快取值,模塊里面的變數系結其所在的模塊,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/332112.html

標籤:其他

上一篇:element時間組件轉格式

下一篇:【通俗易懂】Vue如何使用百度地圖,通過vue-baidu-map插件實作地圖功能【檔案使用介紹】

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more