JS基礎(chǔ)知識(二)

== vs ===

涉及面試題:== 和 === 有什么區(qū)別?

對于 == 來說,如果對比雙方的類型不一樣的話,就會進行類型轉(zhuǎn)換,這也就用到了我們上一章節(jié)講的內(nèi)容。

假如我們需要對比 xy 是否相同,就會進行如下判斷流程:

  1. 首先會判斷兩者類型是否相同。相同的話就是比大小了
  2. 類型不相同的話,那么就會進行類型轉(zhuǎn)換
  3. 會先判斷是否在對比 nullundefined,是的話就會返回 true
  4. 判斷兩者類型是否為 stringnumber,是的話就會將字符串轉(zhuǎn)換為 number
1 == '1'
      ↓
1 ==  1
  1. 判斷其中一方是否為 boolean,是的話就會把 boolean 轉(zhuǎn)為 number 再進行判斷
'1' == true
        ↓
'1' ==  1
        ↓
 1  ==  1
  1. 判斷其中一方是否為 object 且另一方為 stringnumber 或者 symbol,是的話就會把 object 轉(zhuǎn)為原始類型再進行判斷
'1' == { name: 'yck' }
        ↓
'1' == '[object Object]'

思考題:看完了上面的步驟,對于 [] == ![] 你是否能正確寫出答案呢?

[] == ![] //true
//[]作為對象ToPrimitive得到 ''  
//![]作為boolean轉(zhuǎn)換得到0 
//'' == 0 
//轉(zhuǎn)換為 0==0 //true

閉包

涉及面試題:什么是閉包?

閉包的定義其實很簡單:函數(shù) A 內(nèi)部有一個函數(shù) B,函數(shù) B 可以訪問到函數(shù) A 中的變量,那么函數(shù) B 就是閉包。

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

很多人對于閉包的解釋可能是函數(shù)嵌套了函數(shù),然后返回一個函數(shù)。其實這個解釋是不完整的,就比如我上面這個例子就可以反駁這個觀點。

在 JS 中,閉包存在的意義就是讓我們可以間接訪問函數(shù)內(nèi)部的變量。

經(jīng)典面試題,循環(huán)中使用閉包解決 var 定義函數(shù)的問題

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因為 setTimeout 是個異步函數(shù),所以會先把循環(huán)全部執(zhí)行完畢,這時候 i 就是 6 了,所以會輸出一堆 6。

解決辦法有三種,第一種是使用閉包的方式

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

在上述代碼中,我們首先使用了立即執(zhí)行函數(shù)將 i 傳入函數(shù)內(nèi)部,這個時候值就被固定在了參數(shù) j 上面不會改變,當(dāng)下次執(zhí)行 timer 這個閉包的時候,就可以使用外部函數(shù)的變量 j,從而達到目的。

第二種就是使用 setTimeout 的第三個參數(shù),這個參數(shù)會被當(dāng)成 timer 函數(shù)的參數(shù)傳入。

for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    }, i * 1000, i)
}

第三種就是使用 let 定義 i 了來解決問題了,這個也是最為推薦的方式

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

深淺拷貝

涉及面試題:什么是淺拷貝?如何實現(xiàn)淺拷貝?什么是深拷貝?如何實現(xiàn)深拷貝?

在上一章節(jié)中,我們了解了對象類型在賦值的過程中其實是復(fù)制了地址,從而會導(dǎo)致改變了一方其他也都被改變的情況。通常在開發(fā)中我們不希望出現(xiàn)這樣的問題,我們可以使用淺拷貝來解決這個情況。

let a = {
  age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

淺拷貝

首先可以通過 Object.assign 來解決這個問題,很多人認為這個函數(shù)是用來深拷貝的。其實并不是,Object.assign 只會拷貝所有的屬性值到新的對象中,如果屬性值是對象的話,拷貝的是地址,所以并不是深拷貝。

let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

另外我們還可以通過展開運算符 ...(ES6語法,展開操作符除了可以展開淺拷貝對象之外,還可以用來展開數(shù)組) 來實現(xiàn)淺拷貝

let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

通常淺拷貝就能解決大部分問題了,但是當(dāng)我們遇到如下情況就可能需要使用到深拷貝了

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native

淺拷貝只解決了第一層的問題,如果接下去的值中還有對象的話,那么就又回到最開始的話題了,兩者享有相同的地址。要解決這個問題,我們就得使用深拷貝了。

深拷貝

這個問題通??梢酝ㄟ^ JSON.parse(JSON.stringify(object)) 來解決。

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是該方法也是有局限性的:

  • 會忽略 undefined
  • 會忽略 symbol
  • 不能序列化函數(shù)
  • 不能解決循環(huán)引用的對象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

如果你有這么一個循環(huán)引用對象,你會發(fā)現(xiàn)并不能通過該方法實現(xiàn)深拷貝

在遇到函數(shù)、 undefined 或者 symbol 的時候,該對象也不能正常的序列化

let a = {
  age: undefined,
  sex: Symbol('male'),
  jobs: function() {},
  name: 'jwj'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "jwj"}

你會發(fā)現(xiàn)在上述情況中,該方法會忽略掉函數(shù)和 undefined 。

但是在通常情況下,復(fù)雜數(shù)據(jù)都是可以序列化的,所以這個函數(shù)可以解決大部分問題。

如果你所需拷貝的對象含有內(nèi)置類型并且不包含函數(shù),可以使用 MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel()
    port2.onmessage = ev => resolve(ev.data)
    port1.postMessage(obj)
  })
}

var obj = {
  a: 1,
  b: {
    c: 2
  }
}

obj.b.d = obj.b

// 注意該方法是異步的
// 可以處理 undefined 和循環(huán)引用對象
const test = async () => {
  const clone = await structuralClone(obj)
  console.log(clone)
}
test()

當(dāng)然你可能想自己來實現(xiàn)一個深拷貝,但是其實實現(xiàn)一個深拷貝是很困難的,需要我們考慮好多種邊界情況,比如原型鏈如何處理、DOM 如何處理等等,所以這里我們實現(xiàn)的深拷貝只是簡易版,并且我其實更推薦使用 lodash 的深拷貝函數(shù)。

function deepClone(obj) {
  function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
  }

  if (!isObject(obj)) {
    throw new Error('非對象')
  }

  let isArray = Array.isArray(obj)
  let newObj = isArray ? [...obj] : { ...obj }
  Reflect.ownKeys(newObj).forEach(key => {
    newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
  })

  return newObj
}

let obj = {
  a: [1, 2, 3],
  b: {
    c: 2,
    d: 3
  }
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2

還有一種,是從 iview 的源碼中看到的,或者可以去看下 vue.js 精講

function typeOf(obj) {
    const toString = Object.prototype.toString;
    const map = {
        '[object Boolean]'  : 'boolean',
        '[object Number]'   : 'number',
        '[object String]'   : 'string',
        '[object Function]' : 'function',
        '[object Array]'    : 'array',
        '[object Date]'     : 'date',
        '[object RegExp]'   : 'regExp',
        '[object Undefined]': 'undefined',
        '[object Null]'     : 'null',
        '[object Object]'   : 'object'
    };
    return map[toString.call(obj)];
}
// deepCopy
function deepCopy(data) {
    const t = typeOf(data);
    let o;

    if (t === 'array') {
        o = [];
    } else if ( t === 'object') {
        o = {};
    } else {
        return data;
    }

    if (t === 'array') {
        for (let i = 0; i < data.length; i++) {
            o.push(deepCopy(data[i]));
        }
    } else if ( t === 'object') {
        for (let i in data) {
            o[i] = deepCopy(data[i]);
        }
    }
    return o;
}

export {deepCopy};

原型

涉及面試題:如何理解原型?如何理解原型鏈?

當(dāng)我們創(chuàng)建一個對象時 let obj = { age: 25 },我們可以發(fā)現(xiàn)能使用很多種函數(shù),但是我們明明沒有定義過它們,對于這種情況你是否有過疑惑?

原型也是一個對象,并且這個對象中包含了很多函數(shù),所以我們可以得出一個結(jié)論:對于 obj 來說,可以通過 __proto__找到一個原型對象,在該對象中定義了很多函數(shù)讓我們來使用。

打開 constructor 屬性我們又可以發(fā)現(xiàn)其中還有一個 prototype 屬性,并且這個屬性對應(yīng)的值和先前我們在__proto__ 中看到的一模一樣。所以我們又可以得出一個結(jié)論:原型的 constructor 屬性指向構(gòu)造函數(shù),構(gòu)造函數(shù)又通過 prototype 屬性指回原型,但是并不是所有函數(shù)都具有這個屬性,Function.prototype.bind() 就沒有這個屬性。

其實原型鏈就是多個對象通過 __proto__ 的方式連接了起來。為什么 obj 可以訪問到 valueOf 函數(shù),就是因為 obj 通過原型鏈找到了 valueOf函數(shù)。

對于這一小節(jié)的知識點,總結(jié)起來就是以下幾點:

  • Object 是所有對象的爸爸,所有對象都可以通過__proto__找到它
  • Function 是所有函數(shù)的爸爸,所有函數(shù)都可以通過__proto__找到它
  • 函數(shù)的 prototype是一個對象
  • 對象的__proto__屬性指向原型, __proto__將對象和原型連接起來組成了原型鏈
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。