小邵教你玩轉ES6

邵威儒發表於2018-09-12

前言:大家好,我叫邵威儒,大家都喜歡喊我小邵,學的金融專業卻憑藉興趣愛好入了程式猿的坑,從大學買的第一本vb和自學vb,我就與程式設計結下不解之緣,隨後自學易語言寫遊戲輔助、交易軟體,至今進入了前端領域,看到不少朋友都寫文章分享,自己也弄一個玩玩,以下文章純屬個人理解,便於記錄學習,肯定有理解錯誤或理解不到位的地方,意在站在前輩的肩膀,分享個人對技術的通俗理解,共同成長!

後續我會陸陸續續更新javascript方面,儘量把javascript這個學習路徑體系都寫一下
包括前端所常用的es6、angular、react、vue、nodejs、koa、express、公眾號等等
都會從淺到深,從入門開始逐步寫,希望能讓大家有所收穫,也希望大家關注我~

文章列表:juejin.im/user/5a84f8…

Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/


接下來我主要給大家講下我對常用的es6的理解,我們工作當中,其實有很多用不上的,如果想詳細瞭解的話可以看看阮一峰老師的es6:es6.ruanyifeng.com/

這篇文章主要讓你學會工作當中常用的es6技巧,以及擴充套件如實現資料雙向繫結,class用es5如何實現、如何給偽陣列新增迭代器等等。


var、let、const

// 1.var存在變數作用域的提升
console.log(a) // 列印輸出 undefined
var a = 1

// 怎麼理解作用域的提升呢?
// var str = 'hello swr'
// function(){
//     console.log(str) // 列印輸出 undefined
//     var str = 'goodbye swr'
// }
// test()

// 上面這段程式碼實際上是
var str = 'hello swr'
function(){
    var str
    console.log(str) // 列印輸出undefined
                     // 實際上就是var宣告的變數,拿到
                     // 當前作用域的最頂層,而此時尚未賦值
                     // 只是宣告,所以列印出undefined,而非當執行
                     // 到這段程式碼時才宣告,優先宣告,
                     // 當執行到那行的時候,實際上是賦值
                     // 同樣的,function xx(){}也存在作用域提升
    str = 'goodbye swr'
}
test()

// var 不存在塊級作用域的概念
// 我的理解是在es6之前,是沒有塊級作用域的概念,
// 變數只有遇到函式的時候才會變為區域性變數
{
    var str 1 = 'hello swr'
}

console.log(str1) // 列印輸出 hello swr
複製程式碼
// 2.let
// 2.1 不存在變數作用域提升,這樣可以避免了我們還沒宣告變數就拿變數來用
// 2.2 同一作用域的同一個變數不能夠重複宣告,避免我們重複宣告變數
// 2.3 let宣告的變數不會到全域性上
// 2.4 let和程式碼塊{}結合使用會形成塊級作用域

// 2.1
// console.log(a) // 報錯,a未宣告
// let a = 'hello swr'

// 2.2
// let a = 'hello swr'
// let a = 'hello swr' // 報錯,變數被重複宣告

// 2.3
// let a = 'hello swr'
// console.log(window.a) // undefined

// 2.4
// 在程式碼塊以外呼叫str2,會報錯
{
    let str2 = 'hello swr'
}

console.log(str2) // 報錯,未找到變數

// 上面這種寫法,也有點型別es6之前的立即執行函式
(function(){
    var str2 = 'hello swr'
})()

// 一個例子
// 使用var,會發現最終console.log中列印的i都是3
// 因為for迴圈不是函式,而此時var i是處於全域性當中
// for迴圈是同步程式碼,所以會執行完同步程式碼後
// 再執行setTimeout的非同步程式碼,此時i已為3,所以列印出來都是3
for(var i = 0;i < 3;i++){
    setTimeout(function(){
        console.log(i)
    },1000)
}

// 那麼我們用let試下
// let和程式碼塊結合起來使用會形成塊級作用域
// 那麼當for時,這3個setTimeout會分別在3個不同的塊級作用域
// 當執行setTimeout的console.log(i)時,會先尋找最近的塊級作用域中的i
// 所以會依次列印出0 1 2
for(let j = 0;j < 3;j++){
    setTimeout(function(){
        console.log(i)
    },1000)
}
複製程式碼
// 3.const
// 3.1 const和let基本上可以說是完全一致的,但是const宣告的物件不能更改其指向的引用地址(即堆區)


// 3.1
// 當用普通值賦值給const宣告的變數後,再重新賦值時
// 值引用會被更改,所以會報錯
const STR1 = 'hello swr'
STR1 = 'goodbye swr' // 報錯,Assignment to constant variable

// 當我們修改這個引用地址裡面的內容時,則不會報錯
// 因為這個變數是指向這個引用地址的
const OBJ = {name:"swr"}
OBJ.name = 'hello swr'
console.log(OBJ) // {name:"hello swr"}
// 但是當我們把這個變數重新賦值一個引用地址時,則會報錯
OBJ = {} // 報錯
複製程式碼

解構賦值

解構賦值主要分為物件的解構和陣列的解構,在沒有解構賦值的時候,我們賦值是這樣的

let arr = [0,1,2]
let a = arr[0]
let b = arr[1]
let c = arr[2]
複製程式碼

這樣寫很繁瑣,那麼我們有沒辦法既宣告,又賦值,更優雅的寫法呢?肯定是有的,那就是解構賦值,解構賦值,簡單理解就是等號的左邊和右邊相等。

陣列的解構賦值

let arr = [0,1,2]
let [a,b,c] = arr
console.log(a) // 0
console.log(b) // 1
console.log(c) // 2
複製程式碼

但是很多時候,資料並非一一對應的,並且我們希望得到一個預設值

let arr = [,1,2]
let [a='我是預設值',b,c] = arr
console.log(a) // '我是預設值'
console.log(b) // 1
console.log(c) // 2
// 從這個例子可以看出,在解構賦值的過程中,a=undefined時,會使用預設值
// 那麼當a=null時呢?當a=null時,那麼a就不會使用預設值,而是使用null
複製程式碼
// 陣列的拼接
let a = [0,1,2]
let b = [3,4,5]
let c = a.concat(b)
console.log(c) // [0,1,2,3,4,5]

let d = [...a,...b]
console.log(d) // [0,1,2,3,4,5]
複製程式碼
// 陣列的克隆
// 假如我們簡單地把一個陣列賦值給另外一個變數
let a = [0,1,2,3]
let b = a
b.push(4)
console.log(a) // [0,1,2,3,4]
console.log(b) // [0,1,2,3,4]
// 因為這只是簡單的把引用地址賦值給b,而不是重新開闢一個記憶體地址,所以
// a和b共享了同一個記憶體地址,該記憶體地址的更改,會影響到所有引用該地址的變數
// 那麼用下面的方法,把陣列進行克隆一份,互不影響

let a = [0,1,2,3]
let b = [...a]
b.push(4)
console.log(a) // [0,1,2,3]
console.log(b) // [0,1,2,3,4]
複製程式碼

物件的解構賦值

物件的解構賦值和陣列的解構賦值其實類似,但是陣列的陣列成員是有序的
而物件的屬性則是無序的,所以物件的解構賦值簡單理解是等號的左邊和右邊的結構相同

let {name,age} = {name:"swr",age:28}
console.log(name) // 'swr'
console.log(age) // 28
複製程式碼

物件的解構賦值是根據key值進行匹配

// 這裡可以看出,左側的name和右側的name,是互相匹配的key值
// 而左側的name匹配完成後,再賦值給真正需要賦值的Name
let { name:Name,age } = { name:'swr',age:28 }
console.log(Name) // 'swr'
console.log(age) // 28
複製程式碼

那麼當變數已經被宣告瞭呢?

let name,age
// 需要用圓括號,包裹起來
({name,age} = {name:"swr",age:28})
console.log(name) // 'swr'
console.log(age) // 28
複製程式碼

變數能否也設定預設值?

let {name="swr",age} = {age:28}
console.log(name) // 'swr'
console.log(age) // 28
// 這裡規則和陣列的解構賦值一樣,當name = undefined時,則會使用預設值
複製程式碼
let [a] = [{name:"swr",age:28}]
console.log(a) // {name:"swr",age:28}

let { length } = "hello swr"
console.log(length) // 9
複製程式碼
function ajax({method,url,type='params'}){
    console.log(method) // 'get'
    console.log(url) // '/'
    console.log(type) // 'params'
}

ajax({method:"get",url:"/"})
複製程式碼

擴充套件運算子

我們先看下程式碼

// 在以往,我們給函式傳不確定引數數量時,是通過arguments來獲取的
function sum() {
  console.log(arguments) // { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6 }
                         // 我們可以看出,arguments不是一個陣列,而是一個偽陣列
  let total = 0
  let { length } = arguments
  for(let i = 0;i < length;i++){
    total += arguments[i]
  }
  return total
}

console.log(sum(1,2,3,4,5,6)) // 21
複製程式碼
// 接下來我們用擴充套件運算子看看
function sum(...args){ // 使用...擴充套件運算子
    console.log(args) // [ 1, 2, 3, 4, 5, 6 ] args是一個陣列
    return eval(args.join('+'))
}

console.log(sum(1,2,3,4,5,6)) // 21
複製程式碼

得到的args是一個陣列,直接對陣列進行操作會比對偽陣列進行操作更加方便,還有一些注意點需要注意

// 正確的寫法 擴充套件運算子只能放在最後一個引數
function sum(a,b,...args){
    console.log(a) // 1
    console.log(b) // 2
    console.log(args) // [ 3, 4, 5, 6 ]
}

sum(1,2,3,4,5,6)

// 錯誤的寫法 擴充套件運算子只能放在最後一個引數
function sum(...args,a,b){
    // 報錯
}

sum(1,2,3,4,5,6)
複製程式碼

我們可以對比下擴充套件運算子的方便之處

// 以往我們是這樣拼接陣列的
let arr1 = [1,2,3]
let arr2 = [4,5,6]
let arr3 = arr1.concat(arr2)
console.log(arr3) // [ 1, 2, 3, 4, 5, 6 ]

// 現在我們用擴充套件運算子看看
let arr1 = [1,2,3]
let arr2 = [4,5,6]
let arr3 = [...arr1,...arr2]
console.log(arr3) // [ 1, 2, 3, 4, 5, 6 ]
複製程式碼
// 以往我們這樣來取陣列中最大的值
function max(...args){
    return Math.max.apply(null,args)
}
console.log(max(1,2,3,4,5,6)) // 6

// 現在我們用擴充套件運算子看看
function max(...args){
    return Math.max(...args) // 把args [1,2,3,4,5,6]展開為1,2,3,4,5,6
}
console.log(max(1,2,3,4,5,6)) // 6
複製程式碼
// 擴充套件運算子可以把argument轉為陣列
function max(){
    console.log(arguments) // { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6 }
    let arr = [...arguments]
    console.log(arr) // [1,2,3,4,5,6]
}

max(1,2,3,4,5,6)

// 但是擴充套件運算子不能把偽陣列轉為陣列(除了有迭代器iterator的偽陣列,如arguments)
let likeArr = { "0":1,"1":2,"length":2 }
let arr = [...likeArr] // 報錯 TypeError: likeArr is not iterable

// 但是可以用Array.from把偽陣列轉為陣列
let likeArr = { "0":1,"1":2,"length":2 }
let arr = Array.from(likeArr)
console.log(arr) // [1,2]
複製程式碼

物件也可以使用擴充套件運算子

// 以往我們這樣合併物件
let name = { name:"邵威儒" }
let age = { age:28 }
let person = {}
Object.assign(person,name,age)
console.log(person) // { name: '邵威儒', age: 28 }

// 使用擴充套件運算子
let name = { name:"邵威儒" }
let age = { age:28 }
let person = {...name,...age}
console.log(person) // { name: '邵威儒', age: 28 }
複製程式碼

需要注意的是,通過擴充套件運算子和Object.assign對物件進行合併的行為,是屬於淺拷貝,那麼我們在開發當中,經常需要對物件進行深拷貝,接下來我們看看如何進行深拷貝。

// 方法一:利用JSON.stringify和JSON.parse
let swr = {
    name:"邵威儒",
    age:28,
    pets:['小黃']
}

let swrcopy = JSON.parse(JSON.stringify(swr))
console.log(swrcopy) // { name: '邵威儒', age: 28, pets: [ '小黃' ] }
// 此時我們新增swr的屬性
swr.pets.push('旺財')
console.log(swr) // { name: '邵威儒', age: 28, pets: [ '小黃', '旺財' ] }
// 但是swrcopy卻不會受swr影響
console.log(swrcopy) // { name: '邵威儒', age: 28, pets: [ '小黃' ] }
// 這種方式進行深拷貝,只針對json資料這樣的鍵值對有效
// 對於函式等等反而無效,不好用,接著繼續看方法二、三。
複製程式碼
// 方法二:
function deepCopy(fromObj,toObj) { // 深拷貝函式
  // 容錯
  if(fromObj === null) return null // 當fromObj為null
  if(fromObj instanceof RegExp) return new RegExp(fromObj) // 當fromObj為正則
  if(fromObj instanceof Date) return new Date(fromObj) // 當fromObj為Date

  toObj = toObj || {}
  
  for(let key in fromObj){ // 遍歷
    if(typeof fromObj[key] !== 'object'){ // 是否為物件
      toObj[key] = fromObj[key] // 如果為普通值,則直接賦值
    }else{
      if(fromObj[key] === null){
        toObj[key] = null
      }else{
        toObj[key] = new fromObj[key].constructor // 如果為object,則new這個object指向的建構函式
        deepCopy(fromObj[key],toObj[key]) // 遞迴          
      }
    }
  }
  return toObj
}

let dog = {
  name:"小白",
  sex:"公",
  firends:[
    {
      name:"小黃",
      sex:"母"
    }
  ]
}

let dogcopy = deepCopy(dog)
// 此時我們把dog的屬性進行增加
dog.firends.push({name:"小紅",sex:"母"})
console.log(dog) // { name: '小白',
                      sex: '公',
                      firends: [ { name: '小黃', sex: '母' }, { name: '小紅', sex: '母' } ] }
// 當我們列印dogcopy,會發現dogcopy不會受dog的影響
console.log(dogcopy) // { name: '小白',
                          sex: '公',
                          firends: [ { name: '小黃', sex: '母' } ] }

複製程式碼
// 方法三:
let dog = {
  name:"小白",
  sex:"公",
  firends:[
    {
      name:"小黃",
      sex:"母"
    }
  ]
}

function deepCopy(obj) {
  if(obj === null) return null
  if(typeof obj !== 'object') return obj
  if(obj instanceof RegExp) return new RegExp(obj)
  if(obj instanceof Date) return new Date(obj)
  let newObj = new obj.constructor
  for(let key in obj){
    newObj[key] = deepCopy(obj[key])
  }
  return newObj
}

let dogcopy = deepCopy(dog)
dog.firends.push({name:"小紅",sex:"母"})
console.log(dogcopy)
複製程式碼

Object.defineProperty

Object.defineProperty這個並不是es6的語法,這個是給一個物件,新增屬性,但是目前框架很多實用這個方法,來實現資料劫持,也就是資料雙向繫結

// 平時我們這樣給一個物件新增屬性
let obj = {str:"hello swr"}
obj.str = 'goodbye swr'
console.log(obj.str) // 'goodbye swr'
複製程式碼

那麼當我們想在給一個物件,讀取值或寫入值時,進行別的操作,該怎麼做呢?

// 使用Object.defineProperty()
// 接收的第一個引數為物件,第二個引數為屬性名,第三個引數為配置物件
let obj = {}
Object.defineProperty(obj,'name',{
    enumerable:true,// 是否可列舉,預設值 false
                    // 如果為false的話,列印這個obj物件,是看不到name這個屬性
    writable:true,  // 是否可寫,預設值 false
                    // 如果為false的話,給name賦值,不會生效
    configurable:true, // 是否可配置(是否可刪除),預設值 false
                       // 如果為true,delete obj.name,再列印obj,則顯示{}
                       // 如果為false,delete obj.name,再列印obj,則顯示{name:undefined}
   value:'swr', // name對應的值
})

// 上面的寫法其實和下面的寫法是一樣的
let obj = {}
obj.name = 'swr'
複製程式碼

那麼既然一樣,我們有必要寫這麼大串的程式碼嗎?

其實核心是get和set,我們繼續往下看

// 需要注意的是,當使用get set時,則不能使用value和writable
let obj = {}
let str
Object.defineProperty(obj,'name',{
    enumerable:true,
    configurable:true, 
    get(){ // 讀,當我們讀取時,則會執行到get,比如obj.name
        // return 'swr' // 當我們obj.name進行讀取時,會返回'swr'
        return str
    },
    set(newValue){ // 寫,當我們寫入時,則會執行到set,比如obj.name = 'swr'
                   // 並且會把newValue作為引數傳進去
        str = newValue
    }
})

obj.name = 'swr' // 寫入
console.log(obj.name) // 'swr'  // 讀取
複製程式碼

這樣一來,我們可以在get set函式中,寫出對應的業務邏輯,

包括很多框架底層,例如

// 一般不再選擇這樣的寫法
Fn.prototype.xxx = xxx

// 更多的是選擇這樣的寫法
// 這樣的好處就是當讀取值的時候,可以做一系列我們想做的事情
Object.defineProperty(Fn.prototype,'xxx',{...})
複製程式碼

那麼我們實現資料雙向繫結呢?

這個問題在面試當中,會經常問這個問題,但是面試官更希望聽到的是具體底層的實現方式,那麼接下來我們也實現一下吧~ ( 簡陋版的……(#^.^#)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>物件的資料雙向繫結</title>
</head>

<body>
  <input id='input' type="" name="" value="">
  <script>
    let el = document.getElementById('input') // 1. 獲取輸入框的dom節點
    let obj = { // 2. 建立一個物件
      name: ""
    }

    function oberseve(obj) { // 3. 對物件進行觀察
      if (typeof obj !== 'object') return // 3.1 判斷引數是否為物件
      for (let key in obj) { // 3.2 對物件進行遍歷,目的是為了把每個屬性都設定get/set
        defineReactive(obj, key, obj[key])
        oberseve(obj[key]) // 3.3 obj[key] 有可能還是一個函式,需要遞迴,給obj[key]裡的屬性進行設定get/set
      }
    }

    function defineReactive(target, property, value) { // 4. 使用Object.defineProperty
      Object.defineProperty(target, property, {
        get() {
          el.value = value // 4.1 當讀取時,把值賦值給input框
          return value
        },
        set(newVal) {
          el.value = newVal // 4.1 當設定時,把賦值給input框
          value = newVal
        }
      })
    }

    oberseve(obj) // 5.執行該函式,對obj物件裡的屬性進行設定get/set
    el.addEventListener('input', function () { // 6.給輸入框繫結input事件
      obj.name = this.value // 7.當輸入框輸入內容時,我們會把輸入框的
                            //   內容賦值給obj.name,觸發obj.name的set方法
    })
  </script>
</body>
</html>
複製程式碼

當我們在輸入框輸入內容時,再到控制檯輸入obj.name檢視這個值時,會發現列印出"hello swr"

小邵教你玩轉ES6

當我們在控制檯,給obj.name賦值時,會發現輸入框的內容也會作出相應更改

小邵教你玩轉ES6

這樣我們就實現了一個簡陋版的資料雙向繫結了,但是這也是有缺點的,這個只是針對物件進行了資料雙向繫結,而尤大大的Vuejs就是基於Object.defineProperty實現的。

除了Object.defineProperty可以實現資料雙向繫結之外,還有其他方式嗎?

肯定是有其他方式可以實現的,利用es6的proxy代理也可以實現資料雙向繫結,但是目前的框架還是比較少使用這種方式。


Proxy

Proxy代理也可以進行資料劫持,但是和Object.defineProperty不同的是,Proxy是在資料外層套了個殼,然後通過這層殼訪問內部的資料,目前Proxy支援13種方式。

小邵教你玩轉ES6

Proxy,我的理解是在資料外層套了個殼,然後通過這層殼訪問內部的資料,就像下面的圖

小邵教你玩轉ES6

let dog = {
  name:"小黃",
  firends:[{
    name:"小紅"
  }]
}

// 1.首先new一個Proxy物件
let proxy = new Proxy(dog,{ // 2.引數一為需要代理的資料,引數二為上圖可以代理的13種的配置物件
    get(target,property){ // 3.引數1為上面dog物件,引數2為dog的屬性
        console.log('get被監控到了')
        return target[property]
    },
    set(target,property,value){ // 4.引數1為上面dog物件,引數2為dog的屬性,引數3為設定的新值
                                // 有點類似Object.defineProperty
        console.log('set被監控到了')
        target[property] = value
    }
})

// 那麼接下來我們設定一下這個屬性
// dog.name = '小紅'  // set值時,發現不會列印 'set被監控到了'
// dog.name // get值時,發現不會列印 'get被監控到了'

// 思考:為什麼在set/get值的時候不會列印出來我們需要的東西呢?

// 上面說得很明白了,proxy相當於是一個殼,代理我們需要監控的資料,也就是我們要通過proxy來訪問內部資料才會被監控到

proxy.name = '小紅' // 列印輸出 'set被監控到了'
proxy.name // 列印輸出 'get被監控到了'
複製程式碼
// Reflect經常和Proxy搭配使用
// 比如我們上面的例子中
let proxy = new Proxy(dog,{ 
    get(target,property){ 
        console.log('get被監控到了')
        return target[property]
    },
    set(target,property,value){ 
        console.log('set被監控到了')
        // target[property] = value 
        // 這裡的target[property] = value 可以用下面的寫法
        Reflect.set(target,property,value)
    }
})
複製程式碼
// 那麼我們該怎樣實現深度的資料劫持呢?
let dog = {
  name:"小黃",
  firend:{
    name:"小紅"
  }
}

// 我們首先寫一個set方法,希望是通過這樣來呼叫
set(dog.firend,funtion(obj){
    console.log(obj) // { name:"小紅" }  回撥函式中的obj代表的是dog.firend的物件
})
複製程式碼
// 實現
let dog = {
  name:"小黃",
  firend:{
    name:"小紅"
  }
}

function set(obj,callback){
    let proxy = new Proxy(obj,{
        set(target,property,value){
            target[property] = value
        }
    })
    // 最後把proxy傳給我們的回撥函式
    callback(proxy)
}

set(dog.firend,function(obj){
    console.log(obj) // { name:"小紅" } 實際就是從set函式中傳出來的proxy物件
})
複製程式碼

Symbol

在js中,常見的資料型別有undefined null string number boolean object,而es6中,則新增了第七種資料型別symbol。

symbol會生成一個獨一無二的值,為常量

let s1 = Symbol()
let s2 = Symbol()
console.log(s1 === s2) // false

// 因為Symbol生成的是一個獨一無二的值,為常量,一般是作為物件的屬性
let obj = {
  [s1]:1,
  [s2]:2
}

console.log(obj) // { [Symbol()]: 1, [Symbol()]: 2 }
複製程式碼

Symbol.for與Symbol差不多,但是Symbol.for會生成一個唯一的標識

let s1 = Symbol.for('foo')
let s2 = Symbol.for('foo')
console.log(s1 === s2) // true

// 也可以通過Symbol.keyFor把標識找出來
console.log(Symbol.keyFor(s1)) // foo
複製程式碼

Array

Array的常用方法有from reduce map forEach findIndex find every some filter includes等等

用法也很簡單,我主要講一下from和reduce。

Array.from

把偽陣列(包括不含有迭代器的偽陣列)轉化為陣列

// 宣告一個偽陣列
let likeArr = { 0:1,1:2,2:3,length:3 }

// 轉換為陣列
Array.from(likeArr) // [1,2,3]
複製程式碼

那麼我們用前面所說的擴充套件運算子,能夠把偽陣列轉為陣列嗎?

// 宣告一個偽陣列
let likeArr = { 0:1,1:2,2:3,length:3 }

// 用擴充套件運算子轉換為陣列
let arr = [...likeArr] // 報錯 likeArr is not iterable
複製程式碼

likeArr is not iterable意思是,likeArr這個偽陣列沒有迭代器,

那麼可以看出,Array.from和...擴充套件運算子的區別了,

Array.from可以將偽陣列(包含沒有迭代器的偽陣列)轉為陣列,

而...擴充套件運算子只能把擁有迭代器的偽陣列轉為陣列,如arguments、map、set,

那麼我們如果想用...擴充套件運算子轉為陣列,該怎麼辦呢?

// 既然擴充套件運算子只能把有迭代器的偽陣列轉為陣列,
// 那麼我們就給偽陣列新增一個迭代器
// 迭代器iterator需要一個generator生成器生成
// 我們給這個偽陣列新增一個[Symbol.iterator]的迭代器
let likeArr = { 0:1,1:2,2:3,length:3,[Symbol.iterator]:function *() {
  for(let i = 0;i < this.length;i++){
    yield this[i]
  }
} }

console.log([...likeArr]) // [1,2,3]
複製程式碼

reduce

let arr = [1,2,3,4,5]

// 引數一:前一個值
// 引數二:下一個值(當前值)
// 引數三:當前的索引
// 引數四:arr陣列
let total = arr.reduce(function(prev,next,currentIndex,arr){
    return prev + next
})

console.log(total) // 15
複製程式碼
// 那麼reduce是怎樣一個執行流程呢?
// 我們一步步拆解出來看
let arr = [1,2,3,4,5]

// arr會一直是[1,2,3,4,5]
// 第一步:此時的prev為1,next為2,currentIndex為1
let total = arr.reduce(function(prev,next,currentIndex,arr){
    return prev + next // 1+2=3   並且把3當做下一次的prev
})

// 第二步:此時的prev為3,next為3,currentIndex為2
let total = arr.reduce(function(prev,next,currentIndex,arr){
    return prev + next // 3+3=6   並且把6當做下一次的prev
})

// 第三步:此時的prev為6,next為4,currentIndex為3
let total = arr.reduce(function(prev,next,currentIndex,arr){
    return prev + next // 6+4=10   並且把10當做下一次的prev
})

// 第四步:此時的prev為10,next為5,currentIndex為4
let total = arr.reduce(function(prev,next,currentIndex,arr){
    return prev + next // 10+5=15 最終結果會作為返回值返回
})
複製程式碼

那我們自己實現一個reduce,看看是如何實現的

Array.prototype.myReduce = function (callback) {
  let prev = this[0]
  for(let i = 0;i < this.length-1;i++){
    prev = callback(prev,this[i+1],i+1,this)
  }
  return prev
}

let arr = [1,2,3,4,5]
let total = arr.myReduce(function(prev,next,currentIndex,arr){
  console.log(prev,next)
  return prev + next
})

console.log(total) // 15
複製程式碼

map對映

可以把陣列返回成一個對映後的陣列

let arr = [1,2,3].map(item => item+1)
console.log(arr) // [2,3,4]
複製程式碼

find

查詢,查詢到後不再繼續查詢,查詢不到則返回undefined,內部返回true的話,則返回當前item,

let arr = [1,2,3,4]

let val = arr.find(item=>item === 3)
console.log(val) // 3
複製程式碼

every

每個值是否滿足條件,如果是則返回true,如果不是則返回false

let arr = [1,2,3,4]
let isTrue = arr.every(item => {
    return item > 0
})

console.log(isTrue) // true

let isTrue2 = arr.every(item => {
    return item > 2
})

console.log(isTrue2) // false
複製程式碼

some

是否有其中一個值滿足條件,如果是則返回true,如果不是則返回false

let arr = [1,2,3,4]
let isTrue = arr.some(item => {
    return item > 2
})

console.log(isTrue) // true

let isTrue2 = arr.some(item => {
    return item > 4
})

console.log(isTrue2) // false
複製程式碼

filter

過濾,在回撥函式中返回的為false的話,相當於過濾掉當前項,返回一個過濾後的陣列

let arr = [1,2,3,4]

let newArr = arr.filter(item=>{
  return item > 2
})

console.log(newArr) // [3,4]
複製程式碼

includes

基本和some一樣


Set

set是放不重複的項,也就是去重

let set = new Set([1,2,3,4,3,2,1])
console.log(set) // Set { 1, 2, 3, 4 }
複製程式碼

Set有幾個常用的方法,add clear delete entries

// add
let set = new Set([1,2,3,4,3,2,1])
set.add(5)
console.log(set) // Set { 1, 2, 3, 4, 5 }

// 新增一個已有的值,則不會新增進去
set.add(1)
console.log(set) // Set { 1, 2, 3, 4, 5 }

// delete
set.delete(3)
console.log(set) // Set { 1, 2, 4, 5 }

// entries
console.log(set.entries()) // SetIterator { [ 1, 1 ],
                                            [ 2, 2 ],
                                            [ 4, 4 ], 
                                            [ 5, 5 ] }

// clear
set.clear()
console.log(set) // Set {}
複製程式碼

Set常用於去重(並集)

function distinct(arr1,arr2){
    return [...new Set([...arr1,...arr2])]
}

let arr = distinct([1,2,3],[2,3,4,5])
console.log(arr) // [1,2,3,4,5]
複製程式碼

求交集

function intersect(arr1,arr2) {
  // 利用Set裡的方法has,來判斷new Set(arr2)中是否含有item,
  // 如果含有,那麼則是true,當為true時,filter函式則會保留該項
  // 如果沒有,則是false,當為false時,filter函式則不會保留該項
  return arr1.filter(item => new Set(arr2).has(item))
}

console.log(intersect([1,2,3],[2,3,4,5])) // [2,3]
複製程式碼

求差集

function difference(arr1,arr2){
    return arr1.filter(item => !new Set(arr2).has(item))
}

console.log(difference([1,2,3],[2,3,4,5])) // [1]
複製程式碼

Map

也是集合,主要格式是 key => value,同樣是不能放重複的key

// 如果放重複的key會怎樣呢?會被覆蓋
let map = new Map()
map.set('name','邵威儒')
map.set('name','swr')
console.log(map) // Map { 'name' => 'swr' }

// 取的話用get
map.get('name') // 'swr'

// 刪的話用delete
map.delete('name')
console.log(map) // Map {}

// 很多方法和set差不多
複製程式碼
let map = new Map()
map.set('name','邵威儒')
map.set('age',28)
// 一般使用for ... of ... 遍歷
for(let [key,value] of map.entries()){
    console.log(key,value) // name 邵威儒
                           // age 28
}

// 也可以用forEach
map.forEach(item => {
    console.log(item) // 邵威儒
                      // 28
})
複製程式碼

Set我用得最多的就是去重了,實際上Set Map我在開發中還是比較少會用到


Class類

核心還是繼承,而Class我認為是es5物件導向的語法糖。

在看Class之前建議看一下js的物件導向 juejin.im/post/5b8a87…

看完後,我們開始進入es6的class

// 語法
// 宣告一個類
Class Person{ 
    // 在constructor中寫例項屬性、方法
    constructor(){ 
        this.name = "邵威儒" // 例項屬性
        this.say = function(){ // 例項方法
            console.log("我是例項方法上的say")
        }
    }
    // 原型方法
    eat(){
        console.log("我是原型方法上的eat")
    }
    // 靜態方法 也會被繼承
    static myName(){ 
        return "我是靜態方法的myName"
    }
    
    // 在es6中靜態屬性不能這樣寫 static name = "邵威儒"  這樣會報錯
    // 在es7中可以這樣寫static name = "邵威儒"
}

let p = new Person() // new一個物件
console.log(p.name) // 邵威儒
p.eat() // 我是原型方法上的eat
console.log(Person.myName()) // 我是靜態方法的myName
複製程式碼

那麼子類怎麼繼承父類呢?

// 父類
class Person{
    constructor(){
        this.name = "swr"
    }
    static myName(){
        return "Person"
    }
    eat(){
        console.log('eat')
    }
}

// 子類
// 子類Child繼承父類Person
// class Child extends Person實際上相當於
// Child.prototype = Object.create(Person.prototype)
// 列印出來可以看到
// console.log(Child.prototype === Person.prototype) // false
// console.log(Child.prototype.__proto__ === Person.prototype) // true
class Child extends Person{
    constructor(){
        super() // 此處的super相當於Person.call(this)
    }
}

複製程式碼

前面我說了Class就型別es5物件導向的語法糖,為什麼這樣說呢?

接下來我們看一下通過es5怎麼模擬實現一個Class(可以用babel轉一下,看看轉為es5的程式碼是怎樣的)

let Child = (function(){
    // 這種閉包的寫法,好處可以把作用域封閉起來 
    // 在Child建構函式外寫一系列變數
    // 如 let name = "邵威儒";let age = 28 等等…
    function Child(){
        console.log(this) // 列印內部this,看看指向哪裡
    }
    return Child
})()

// 通過直接呼叫函式,看看什麼情況
console.log(Child()) // 此時裡面的this是指向全域性的

// 通過new來生成物件
console.log(new Child()) // 此時裡面的this是指向這個new出來的新物件
複製程式碼

在es6中,不使用new來呼叫類,會報錯 Class constructor Child cannot be invoked without 'new'

class Child {
    
}

Child() // TypeError: Class constructor Child cannot be invoked without 'new'
複製程式碼

也就是說,想在es5中,模擬類,那麼沒使用new來呼叫建構函式時,也要丟擲一個錯誤,那麼我們會想到類的校驗方法

// * 1.宣告一個類的校驗方法
// *   引數一:指向的建構函式
// *   引數二:被呼叫時,this的指向
function _classCallCheck(constructor,instance) {
  // * 2.如果這個instance指向的不是constructor的話,意味著不是通過new來呼叫建構函式
  if(!(instance instanceof constructor)){
    // * 3.不滿足時,則丟擲異常
    throw TypeError("Class constructor Child cannot be invoked without 'new'")
  }
}

let Child = (function(){
  function Child(){
    // * 4.在呼叫該建構函式的時候,先執行以下類的校驗方法
    _classCallCheck(Child,this)
  }
  return Child
})()

// 不通過new呼叫時,會報錯
Child() // 報錯 Class constructor Child cannot be invoked without 'new'
複製程式碼

那麼我們類上,有例項屬性方法、原型屬性方法、靜態屬性方法

function _classCallCheck(constructor,instance) {
  if(!(instance instanceof constructor)){
    throw TypeError("Class constructor Child cannot be invoked without 'new'")
  }
}

// * 4.描述器 descriptor
//     引數一:建構函式
//     引數二:描述原型屬性方法陣列
//     引數三:描述靜態屬性方法陣列
function _createClass(constructor,protoProperties,staticProperties) {
  // * 5.如果protoProperties陣列有陣列成員
  if(protoProperties.length){
    // * 6.遍歷
    for(let i = 0;i < protoProperties.length;i++){
      // * 7.通過Object.defineProperty把屬性方法新增到constructor的原型物件上
      Object.defineProperty(constructor.prototype,protoProperties[i].key,{
        // * 8.利用擴充套件運算子,把{key:"say",value:function(){console.log("hello swr")}}展開
        ...protoProperties[i]
      })
    }
  }
}

// * 1.例項屬性方法、原型屬性方法、靜態屬性方法
//     在es6中,原型屬性方法不是通過prototype實現的
//     而是通過一個叫描述器的東西實現的
let Child = (function(){
  function Child(){
    _classCallCheck(Child,this)
    // * 2.例項屬性方法還是寫在建構函式內
    this.name = '邵威儒'
  }
  // * 3.描述器 descriptor
  //     引數一:建構函式
  //     引數二:描述原型屬性方法
  //     引數三:描述靜態屬性方法
  _createClass(Child,
    [
      {key:"say",value:function(){console.log("hello swr")}},
      {key:"myname",value:"iamswr"}
    ],
    [
      {key:"total",value:function(){return 100}}
    ])
  return Child
})()


// * 9.最後我們new一個物件出來,並且呼叫原型屬性方法,看能否呼叫成功
let c = new Child()
c.say() // 'hello swr'    呼叫成功
複製程式碼

接下來,我們把靜態方法,staticProperties也處理一下,
此時會發現,protoProperties和staticProperties都會遍歷然後使用Object.defineProperty
那麼我們封裝一個方法進行處理

function _classCallCheck(constructor,instance) {
  if(!(instance instanceof constructor)){
    throw TypeError("Class constructor Child cannot be invoked without 'new'")
  }
}

// * 1.封裝一個方法,處理遍歷和Object.defineProperty
function _defineProperty(target,properties) {
  for (let i = 0; i < properties.length; i++) {
    Object.defineProperty(target, properties[i].key, {
      ...properties[i]
    })
  }
}

function _createClass(constructor,protoProperties,staticProperties) {
  if(protoProperties.length){
    _defineProperty(constructor.prototype, protoProperties)
  }
  // * 2.如果staticProperties陣列有陣列成員
  if(staticProperties.length){
    // * 3.靜態方法需要新增在constructor
    _defineProperty(constructor, staticProperties)
  }
}

let Child = (function(){
  function Child(){
    _classCallCheck(Child,this)
    this.name = '邵威儒'
  }
  _createClass(Child,
    [
      {key:"say",value:function(){console.log("hello swr")}},
      {key:"myname",value:"iamswr"}
    ],
    [
      {key:"total",value:function(){return 100}}
    ])
  return Child
})()


let c = new Child()
c.say()
// * 4.最後我們通過Child來呼叫靜態方法
console.log(Child.total())  // 100
複製程式碼

這樣完成了一個雛形,但是還有最重要的繼承還沒實現,接下來我們實現繼承。

function _classCallCheck(constructor,instance) {
  if(!(instance instanceof constructor)){
    throw TypeError("Class constructor Parent cannot be invoked without 'new'")
  }
}

function defineProperty(target,properties) {
  for (let i = 0; i < properties.length; i++) {
    Object.defineProperty(constructor.prototype, properties[i].key, {
      ...properties[i]
    })
  }
}

function _createClass(constructor,protoProperties,staticProperties) {
  if(protoProperties.length){
    defineProperty(constructor.prototype, protoProperties)
  }
  if(staticProperties.length){
    defineProperty(constructor, staticProperties)
  }
}

// * 6.繼承方法
function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
  }

  // * 7.把子類的原型物件指向新的原型物件 組合寄生式繼承 繼承原型屬性方法
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass, // 把constructor指向子類
      enumerable: false,
      writable: true,
      configurable: true
    }
  });

  // * 8.繼承父類的靜態方法
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

// * 1.父類
let Parent = (function(){
  function Parent(){
    _classCallCheck(Parent,this)
    this.name = '父類例項屬性'
  }
  _createClass(Parent,
    [
      {key:"say",value:function(){console.log("父類原型方法say")}},
      {key:"myname",value:"父類原型屬性myname"}
    ],
    [
      {key:"total",value:function(){return 100}}
    ])
  return Parent
})()

// * 2.子類
let Child = (function (Parent) { // * 4.這裡接收傳進的引數 父類
  // * 5.寫一個繼承方法,繼承原型屬性方法和靜態方法
  _inherits(Child, Parent);
  function Child() {
    _classCallCheck(Child, this)
    // * 9.繼承例項屬性方法
    return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
  }
  return Child
})(Parent) // * 3.在這裡通過傳參,把父類傳進去

let c = new Child()
console.log(c.name) // '父類例項屬性'
複製程式碼

這樣就可以用es5模擬es6的class了,會發現其實es6的class是es5物件導向的一個語法糖,經過這樣解剖一下原始碼實現,會對class有更深刻的理解。

還有個問題,我們在react中,會這樣寫class

class Parent{
    name = "邵威儒"
}
// 在正常情況下會報錯,但是因為平時專案是使用了babel外掛
// 會幫我們自動編譯語法,這種寫法目前還處於草案階段
// 上面的寫法實際等價於下面的寫法
class Parent{
    constructor(){
        this.name = "邵威儒"
    }
}
複製程式碼

decorator 裝飾器

裝飾器是用來裝飾類的

class Person {

}

function myFunction(target) {
  target['myName'] = "邵威儒"
}
myFunction(Person)
console.log(Person['myName']) // 邵威儒
複製程式碼

這種寫法,相當於給Person這個類新增了myName的屬性
那麼換成decorator該怎麼寫呢?

// 在類前面寫@myFunction
@myFunction
class Person {

}

function myFunction(target) {
  target['myName'] = "邵威儒"
}
// myFunction(Person)  這一步可以不寫
console.log(Person['myName']) // 邵威儒
複製程式碼

那麼我們該怎麼給myName傳參呢?

@myFunction('邵威儒')
class Person {

}

function myFunction(value) {
  return function(target){ // target代表的是類
      target['myName'] = value
  }
}
console.log(Person['myName']) // 邵威儒
複製程式碼

修飾符也可以修飾類的方法

class Person {
    @myFunction
    say(){}
}

// 如果修飾的是方法
// 引數一:是Person.prototype
// 引數二:是say
// 引數三:是描述器
function myFunction(target,key,descriptor){
    // 給這個類新增一個原型屬性
    Object.assign(target,{name:"邵威儒"})
}

let p = new Person()
console.log(p.name) // 邵威儒
複製程式碼

修飾符也可以修飾類的屬性,比如我們有個不可修改的屬性

class Person {
    @onlyRead
    name = '邵威儒'
}

function onlyRead(target,key,descriptor){
    descriptor.writable = false
}

let p = new Person()
p.name = 'swr' // 報錯,不能賦值
複製程式碼

decorator的用處很多,包括重寫函式

function myFunction(target,key,descriptor){
    // 拿出原本的函式
    let fn = descriptor.value
    // 並且在原有的fn上加上自己的業務邏輯,比如console.log('哈哈哈')
    descriptor.value = function(){
        // 這裡寫我們需要加入的內容
        console.log('哈哈哈')
        // 這裡執行原來的fn
        fn()
    }
}
複製程式碼

裝飾器經常在react中使用~其實decorator是簡寫,逼格高一些。


es6其實還有很多新語法,但是我們平時並不常用,所以也沒一一列舉,可以到阮大神的es6看看~

相關文章