前端面試中常考的原始碼實現

godbmw發表於2019-03-31

? 內容速覽 ?

  • 手動實現call/apply/bind
  • 實現深拷貝函式
  • 基於ES5/ES6實現雙向繫結
  • instanceof原理與實現
  • 實現支援繫結、解綁和派發的事件類

?檢視全部教程 / 閱讀原文?

手動擼個call/apply/bind

實現call

來看下call的原生表現形式:

function test(arg1, arg2) {
  console.log(arg1, arg2)
  console.log(this.a, this.b)
}

run.call({
  a: 'a',
  b: 'b'
}, 1, 2)
複製程式碼

好了,開始手動實現我們的call2。在實現的過程有個關鍵:

如果一個函式作為一個物件的屬性,那麼通過物件的.運算子呼叫此函式,this就是此物件

let obj = {
  a: 'a',
  b: 'b',
  test: function (arg1, arg2) {
    console.log(arg1, arg2)
    // this.a 就是 a; this.b 就是 b
    console.log(this.a, this.b) 
  }
}

obj.test(1, 2)
複製程式碼

知道了實現關鍵,下面就是我們模擬的call

Function.prototype.call2 = function(context) {
  if(typeof this !== 'function') {
    throw new TypeError('Error')
  }

  // 預設上下文是window
  context = context || window
  // 儲存預設的fn
  const { fn } = context

  // 前面講的關鍵,將函式本身作為物件context的屬性呼叫,自動繫結this
  context.fn = this
  const args = [...arguments].slice(1)
  const result = context.fn(...args)
  
  // 恢復預設的fn
  context.fn = fn
  return result
}

// 以下是測試程式碼
function test(arg1, arg2) {
  console.log(arg1, arg2)
  console.log(this.a, this.b)
}

test.call2({
  a: 'a',
  b: 'b'
}, 1, 2)
複製程式碼

實現apply

applycall實現類似,只是傳入的引數形式是陣列形式,而不是逗號分隔的引數序列。

因此,藉助es6提供的...運算子,就可以很方便的實現陣列和引數序列的轉化。

Function.prototype.apply2 = function(context) {
  if(typeof this !== 'function') {
    throw new TypeError('Error')
  }

  context = context || window
  const { fn } = context

  context.fn = this
  let result
  if(Array.isArray(arguments[1])) {
    // 通過...運算子將陣列轉換為用逗號分隔的引數序列
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  context.fn = fn
  return result
}

/**
 * 以下是測試程式碼
 */

function test(arg1, arg2) {
  console.log(arg1, arg2)
  console.log(this.a, this.b)
}

test.apply2({
  a: 'a',
  b: 'b'
}, [1, 2])
複製程式碼

實現bind

bind的實現有點意思,它有兩個特點:

  • 本身返回一個新的函式,所以要考慮new的情況
  • 可以“保留”引數,內部實現了引數的拼接
Function.prototype.bind2 = function(context) {
  if(typeof this !== 'function') {
    throw new TypeError('Error')
  }

  const that = this
  // 保留之前的引數,為了下面的引數拼接
  const args = [...arguments].slice(1)

  return function F() {
    // 如果被new建立例項,不會被改變上下文!
    if(this instanceof F) {
      return new that(...args, ...arguments)
    }
	
    // args.concat(...arguments): 拼接之前和現在的引數
    // 注意:arguments是個類Array的Object, 用解構運算子..., 直接拿值拼接
    return that.apply(context, args.concat(...arguments))
  }
}

/**
 * 以下是測試程式碼
 */

function test(arg1, arg2) {
  console.log(arg1, arg2)
  console.log(this.a, this.b)
}

const test2 = test.bind2({
  a: 'a',
  b: 'b'
}, 1) // 引數 1

test2(2) // 引數 2
複製程式碼

實現深拷貝函式

實現一個物件的深拷貝函式,需要考慮物件的元素型別以及對應的解決方案:

  • 基礎型別:這種最簡單,直接賦值即可
  • 物件型別:遞迴呼叫拷貝函式
  • 陣列型別:這種最難,因為陣列中的元素可能是基礎型別、物件還可能陣列,因此要專門做一個函式來處理陣列的深拷貝
/**
 * 陣列的深拷貝函式
 * @param {Array} src 
 * @param {Array} target 
 */
function cloneArr(src, target) {
  for(let item of src) {
    if(Array.isArray(item)) {
      target.push(cloneArr(item, []))
    } else if (typeof item === 'object') {
      target.push(deepClone(item, {}))
    } else {
      target.push(item)
    }
  }
  return target
}

/**
 * 物件的深拷貝實現
 * @param {Object} src 
 * @param {Object} target 
 * @return {Object}
 */
function deepClone(src, target) {
  const keys = Reflect.ownKeys(src)
  let value = null

  for(let key of keys) {
    value = src[key]
    
    if(Array.isArray(value)) {
      target[key] = cloneArr(value, [])
    } else if (typeof value === 'object') {
      // 如果是物件而且不是陣列, 那麼遞迴呼叫深拷貝
      target[key] = deepClone(value, {})
    } else {
      target[key] = value
    }
  }

  return target
}
複製程式碼

這段程式碼是不是比網上看到的多了很多?因為考慮很周全,請看下面的測試用例:

// 這個物件a是一個囊括以上所有情況的物件
let a = {
  age: 1,
  jobs: {
    first: "FE"
  },
  schools: [
    {
      name: 'shenda'
    },
    {
      name: 'shiyan'
    }
  ],
  arr: [
    [
      {
        value: '1'
      }
    ],
    [
      {
        value: '2'
      }
    ],
  ]
};

let b = {}
deepClone(a, b)

a.jobs.first = 'native'
a.schools[0].name = 'SZU'
a.arr[0][0].value = '100'

console.log(a.jobs.first, b.jobs.first) // output: native FE
console.log(a.schools[0], b.schools[0]) // output: { name: 'SZU' } { name: 'shenda' }
console.log(a.arr[0][0].value, b.arr[0][0].value) // output: 100 1
console.log(Array.isArray(a.arr[0])) // output: true
複製程式碼

看到測試用例,應該會有人奇怪為什麼最後要輸出Array.isArray(a.arr[0])。這主要是因為網上很多實現方法沒有針對array做處理,直接將其當成object,這樣拷貝後雖然值沒問題,但是array的元素會被轉化為object。這顯然是錯誤的做法。

而上面所說的深拷貝函式就解決了這個問題。

基於ES5/ES6實現“雙向繫結”

要想實現,就要先看看什麼是“雙向資料繫結”,它和“單向資料繫結”有什麼區別?這樣才能知道要實現什麼效果嘛。

雙向繫結:檢視(View)的變化能實時讓資料模型(Model)發生變化,而資料的變化也能實時更新到檢視層。

單向資料繫結:只有從資料到檢視這一方向的關係。

ES5的Object.defineProperty

<!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>Document</title>
  <script>
      const obj = {
        value: ''
      }
      
      function onKeyUp(event) {
        obj.value = event.target.value
      }
      
      // 對 obj.value 進行攔截
      Object.defineProperty(obj, 'value', {
        get: function() {
          return value
        },
        set: function(newValue) {
          value = newValue
          document.querySelector('#value').innerHTML = newValue // 更新檢視層
          document.querySelector('input').value = newValue // 資料模型改變
        }
      })
    </script>
</head>
<body>
  <p>
    值是:<span id="value"></span>
  </p>
  <input type="text" onkeyup="onKeyUp(event)">
</body>
</html>
複製程式碼

ES6的Proxy

隨著,vue3.0放棄支援了IE瀏覽器。而且Proxy相容性越來越好,能支援13種劫持操作。

因此,vue3.0選擇使用Proxy來實現雙向資料繫結,而不再使用Object.defineProperty

<!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>Document</title>
  <script>
    const obj = {}

    const newObj = new Proxy(obj, {
      get: function(target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set: function(target, key, value, receiver) {
        if(key === 'value') {
          document.querySelector('#value').innerHTML = value
          document.querySelector('input').value = value
        }
        return Reflect.set(target, key, value, receiver)
      }
    })
    
    function onKeyUp(event) {
      newObj.value = event.target.value
    }
    
  </script>
</head>
<body>
  <p>
    值是:<span id="value"></span>
  </p>
  <input type="text" onkeyup="onKeyUp(event)">
</body>
</html>
複製程式碼

instanceof原理與實現

instanceof是通過原型鏈來進行判斷的,所以只要不斷地通過訪問__proto__,就可以拿到建構函式的原型prototype。直到null停止。

/**
 * 判斷left是不是right型別的物件
 * @param {*} left 
 * @param {*} right 
 * @return {Boolean}
 */
function instanceof2(left, right) {
  let prototype = right.prototype;

  // 沿著left的原型鏈, 看看是否有何prototype相等的節點
  left = left.__proto__;
  while(1) {
    if(left === null || left === undefined) {
      return false;
    }
    if(left === prototype) {
      return true;
    }
    left = left.__proto__;
  }
}

/**
 * 測試程式碼
 */

console.log(instanceof2([], Array)) // output: true

function Test(){}
let test = new Test()
console.log(instanceof2(test, Test)) // output: true
複製程式碼

實現支援繫結、解綁和派發的事件類

實現思路:這裡涉及了“訂閱/釋出模式”的相關知識。參考addEventListener(type, func)removeEventListener(type, func)的具體效果來實現即可。

// 陣列置空:
// arr = []; arr.length = 0; arr.splice(0, arr.length)
class Event {
  constructor() {
    this._cache = {};
  }

  // 註冊事件:如果不存在此種type,建立相關陣列
  on(type, callback) {
    this._cache[type] = this._cache[type] || [];
    let fns = this._cache[type];
    if (fns.indexOf(callback) === -1) {
      fns.push(callback);
    }
    return this;
  }

  // 觸發事件:對於一個type中的所有事件函式,均進行觸發
  trigger(type, ...data) {
    let fns = this._cache[type];
    if (Array.isArray(fns)) {
      fns.forEach(fn => {
        fn(...data);
      });
    }
    return this;
  }

  // 刪除事件:刪除事件型別對應的array
  off(type, callback) {
    let fns = this._cache[type];
    // 檢查是否存在type的事件繫結
    if (Array.isArray(fns)) {
      if (callback) {
        // 解除安裝指定的回撥函式
        let index = fns.indexOf(callback);
        if (index !== -1) {
          fns.splice(index, 1);
        }
      } else {
        // 全部清空
        fns = [];
      }
    }
    return this;
  }
}

// 以下是測試函式

const event = new Event();
event
  .on("test", a => {
    console.log(a);
  })
  .trigger("test", "hello");
複製程式碼

更多系列文章

⭐在GitHub上收藏/訂閱⭐

《前端知識體系》

《設計模式手冊》

《Webpack4漸進式教程》

⭐在GitHub上收藏/訂閱⭐

相關文章