求職之手寫程式碼-手寫原始碼大雜燴

睡魔的謊言發表於2020-11-16

自定義事件

面試官:手寫一個自定義原生事件。

簡單三步曲:

  1. 建立自定義事件:const myEvent = new Event('jsliangEvent')
  2. 監聽自定義事件:document.addEventListener(jsliangEvent)
  3. 觸發自定義事件:document.dispatchEvent(jsliangEvent)

簡單實現:

window.onload = function() {
  const myEvent = new Event('jsliangEvent');
  document.addEventListener('jsliangEvent', function(e) {
    console.log(e);
  })
  setTimeout(() => {
    document.dispatchEvent(myEvent);
  }, 2000);
};
複製程式碼

頁面 2 秒後自動觸發 myEvent 事件。

建立自定義事件

建立自定義事件的 3 種方法:

  • 使用 Event
let myEvent = new Event('event_name');
複製程式碼
  • 使用 customEvent(可以傳引數)
let myEvent = new CustomEvent('event_name', {
  detail: {
    // 將需要傳遞的引數放到這裡
    // 可以在監聽的回撥函式中獲取到:event.detail
  }
});
複製程式碼
  • 使用 document.createEvent('CustomEvent')initEvent()
// createEvent:建立一個事件
let myEvent = document.createEvent('CustomEvent'); // 注意這裡是 CustomEvent

// initEvent:初始化一個事件
myEvent.initEvent(
  // 1. event_name:事件名稱
  // 2. canBubble:是否冒泡
  // 3. cancelable:是否可以取消預設行為
)
複製程式碼

事件的監聽

自定義事件的監聽其實和普通事件一樣,通過 addEventListener 來監聽:

button.addEventListener('event_name', function(e) {})
複製程式碼

事件的觸發

觸發自定義事件使用 dispatchEvent(myEvent)

注意,這裡的引數是要自定義事件的物件(也就是 myEvent),而不是自定義事件的名稱(myEvent

案例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>自定義事件</title>
</head>
<body>
  <button class="btn">點我</button>
  
  <script>
    window.onload = function() {
      // 方法 1
      const myEvent = new Event('myEvent');

      // 方法 2
      // const myEvent = new CustomEvent('myEvent', {
      //   detail: {
      //     name: 'jsliang',
      //   },
      // });

      // 方法 3
      // const myEvent = document.createEvent('CustomEvent');
      // myEvent.initEvent('myEvent', true, true);

      const btn = document.querySelector('.btn');
      btn.addEventListener('myEvent', function(e) {
        console.log(e);
      })
      setTimeout(() => {
        btn.dispatchEvent(myEvent);
      }, 2000);
    };
  </script>
</body>
</html>
複製程式碼

上面 console.log(e) 輸出:

/*
  CustomEvent {
    bubbles: true
    cancelBubble: false
    cancelable: true
    composed: false
    currentTarget: null
    defaultPrevented: false
    detail: null
    eventPhase: 0
    isTrusted: false
    path: (5) [button.btn, body, html, document, Window]
    returnValue: true
    srcElement: button.btn
    target: button.btn
    timeStamp: 16.354999970644712
    type: "myEvent"
  }
*/
複製程式碼

Object.create()

Object.create() 方法建立一個新物件,使用現有的物件來提供新建立的物件的 __proto__

function create(proto) {
  function F() {};
  F.prototype = proto;
  return new F();
}
複製程式碼

試驗一下:

function create(proto) {
  function F() {};
  F.prototype = proto;
  return new F();
}

const Father = function() {
  this.bigName = '爸爸';
};
Father.prototype.sayHello = function() {
  console.log(`我是你${this.bigName}`);
}

const Child = function() {
  Father.call(this);
  this.smallName = '兒子';
}
Child.prototype = create(Father.prototype);
Child.prototype.constructor = Child;

const child = new Child();
child.sayHello(); // 我是你爸爸
複製程式碼

下面講寄生組合式繼承會用到 Object.create()

ES5 實現類繼承

使用 ES5 實現繼承,簡要在 3 行程式碼:

  1. Father.call(this)。在 Child 中通過 Father.call(this),將 Fatherthis 修改為 Childthis
  2. Child.prototype = Object.create(Father.prototype)。將 Child 的原型鏈繫結到 Father 的原型鏈上。
  3. Child.prototype.constructor = Child。這個建構函式的例項的構造方法 constructor 指向自身。
const Father = function (name, like) {
  this.name = name;
  this.like = like;
  this.money = 10000000;
};

Father.prototype.company = function() {
  console.log(`${this.name} 有 ${this.money} 元`);
}

const Children = function (name, like) {
  Father.call(this);
  this.name = name;
  this.like = like;
}

Children.prototype = Object.create(Father.prototype);
Children.prototype.constructor = Children;

const jsliang = new Children('jsliang', '學習');
console.log(jsliang); // Children {name: "jsliang", like: "學習", money: 10000000}
jsliang.company(); // jsliang 有 10000000 元
複製程式碼

需要注意 Child.prototype = Object.create(Father.prototype) 這句話:

  1. 這一步不用 Child.prototype = Father.prototype 的原因是怕共享記憶體,修改父類原型物件就會影響子類
  2. 不用 Child.prototype = new Parent() 的原因是會呼叫 2 次父類的構造方法(另一次是 call),會存在一份多餘的父類例項屬性
  3. Object.create 是建立了父類原型的副本,與父類原型完全隔離

最後,這種繼承方法,叫做 寄生組合式繼承

instanceof

面試官:手寫一個 instanceof,其實 instanceof 就是查詢原型鏈的過程.

那麼有下面程式碼:

const Father = function() {
  this.bigName = '爸爸';
};
Father.prototype.sayHello = function() {
  console.log(`我是你${this.bigName}`);
}

const Child = function() {
  Father.call(this);
  this.smallName = '兒子';
}
Child.prototype = Object.create(Father.prototype);
Child.prototype.constructor = Child;

const child = new Child();
child.sayHello(); // 我是你爸爸

console.log(child instanceof Child); // true
console.log(child instanceof Father); // true
console.log(child instanceof Object); // true
複製程式碼

如何改造當中的 instanceof 呢?

function instanceOf(a, b) {
  let proto = a.__proto__;
  const prototype = b.prototype;

  // 從當前 __proto__ 開始查詢
  while (proto) {
    
    // 如果找到 null 還沒有找到,返回 false
    if (proto === null) {
      return false;
    }

    // 如果 a.__proto__.xxx === b.prototype返回 true
    if (proto === prototype) {
      return true;
    }

    // 進一步迭代
    proto = proto.__proto__;
  }
}

console.log(instanceOf(child, Child)); // true
console.log(instanceOf(child, Father)); // true
console.log(instanceOf(child, Object)); // true
複製程式碼

輸出結果同 instanceof 一樣,完成目標!

才怪!!!

經過測試:

let num = 123;
console.log(num instanceof Object); // false
console.log(instancOf(123, Object)); // true
複製程式碼

為什麼呢?因為 instanceof 在原生程式碼上,實際是做了基本型別的檢測,基本型別應該返回 false,所以可以進行改造:

function instanceOf(a, b) {
  // 新增:通過 typeof 判斷基本型別
  if (typeof a !== 'object' || b === null) {
    return false;
  }

  // 新增:getPrototypeOf 是 Object 自帶的一個方法
  // 可以拿到引數的原型物件
  let proto = Object.getPrototypeOf(a);
  const prototype = b.prototype;

  while (proto) {
    if (proto === null) {
      return false;
    }
    if (proto === prototype) {
      return true;
    }
    proto = proto.__proto__;
  }
}
複製程式碼

柯里化

實現一個 add 方法,使計算結果能夠滿足以下預期:

add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
複製程式碼

實現方法:

function add () {
  const numberList = Array.from(arguments);

  // 進一步收集剩餘引數
  const calculate = function() {
    numberList.push(...arguments);
    return calculate;
  }

  // 利用 toString 隱式轉換,最後執行時進行轉換
  calculate.toString = function() {
    return numberList.reduce((a, b) => a + b, 0);
  }

  return calculate;
}

// 實現一個 add 方法,使計算結果能夠滿足以下預期
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2, 3)(4)); // 10;
console.log(add(1)(2)(3)(4)(5)); // 15;
複製程式碼

詳細看 JavaScript 系列的閉包篇章,裡面有講解到閉包和柯里化。

迭代器

迭代器的意思是:我的版本是可控的,你踢我一下,我動一下。

// 在資料獲取的時候沒有選擇深拷貝內容
// 對於引用型別進行處理會有問題
// 這裡只是演示簡化了一點
function Iterdtor(arr) {
  let data = [];

  if (!Array.isArray(arr)) {
    data = [arr];
  } else {
    data = arr;
  }

  let length = data.length;
  let index = 0;

  // 迭代器的核心 next
  // 當呼叫 next 的時候會開始輸出內部物件的下一項
  this.next = function () {
    let result = {};
    result.value = data[index];
    result.done = index === length - 1 ? true : false;
    if (index !== length) {
      index++;
      return result;
    }

    // 當內容已經沒有了的時候返回一個字串提示
    return 'data is all done';
  };
}

const arr = [1, 2, 3];

// 生成一個迭代器物件
const iterdtor = new Iterdtor(arr);
console.log(iterdtor.next()); // { value: 1, done: false }
console.log(iterdtor.next()); // { value: 2, done: false }
console.log(iterdtor.next()); // { value: 2, done: true }
console.log(iterdtor.next()); // data is all done
複製程式碼

 Ajax

通過 Promise 實現 ajax

index.json

{
  "name": "jsliang",
  "age": 25
}
複製程式碼

index.js

const getData = (url) => {
  return new Promise((resolve, reject) => {
    // 設定 XMLHttpRequest 請求
    const xhr = new XMLHttpRequest();

    // 設定請求方法和 url
    xhr.open('GET', url);

    // 設定請求頭
    xhr.setRequestHeader('Accept', 'application/json');

    // 設定請求的時候,readyState 屬性變化的一個監控
    xhr.onreadystatechange = (res) => {

      // 如果請求的 readyState 不為 4,說明還沒請求完畢
      if (xhr.readyState !== 4) {
        return;
      }

      // 如果請求成功(200),那麼 resolve 它,否則 reject 它
      if (xhr.status === 200) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    };

    // 傳送請求
    xhr.send();
  })
};
getData('./index.json').then((res) => {
  console.log(res); // { "name": "jsliang", "age": 25 }
})
複製程式碼

補充:Ajax 狀態

  • 0 - 未初始化。尚未呼叫 open() 方法
  • 1 - 啟動。已經呼叫 open() 方法,但尚未呼叫 send() 方法。
  • 2 - 傳送。已經呼叫 send() 方法,但尚未接收到響應。
  • 3 - 接收。已經接收到部分響應資料。
  • 4 - 完成。已經接收到全部響應資料,而且已經可以在客戶端使用了。

陣列扁平化

方法一:手撕遞迴

const jsliangFlat = (arr) => {
  // 1. 設定空陣列
  const result = [];

// 2. 設定遞迴
const recursion = (tempArr) => {
// 2.1 遍歷陣列
for (let i = 0; i < tempArr.length; i++) {
// 2.2 如果陣列裡面還是一個陣列,那麼遞迴它
if (Array.isArray(tempArr[i])) {
recursion(tempArr[i]);
} else { // 2.3 否則新增它
result.push(tempArr[i]);
}
}
};
recursion(arr);

// 3. 返回結果
return result;
};

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

方法二:flat()

flat 方法可以扁平陣列,如果不傳引數,flat() 扁平一層,flat(2) 扁平 2 層,到 flat(Infinity) 扁平所有層。

const jsliangFlat = (arr) => {
  return arr.flat(Infinity);
};

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

注意這個方法在 Node 執行會報錯,這是一個 ES6 的方法。

方法三:reduce

不推薦 reduce,我怕小夥伴看得頭暈。

const jsliangFlat = (arr = []) => {
  return arr.reduce((prev, next) => {
    if (Array.isArray(next)) {
      return prev.concat(jsliangFlat(next));
    } else {
      return prev.concat(next);
    }
  }, [])
};

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

物件扁平化

其實我也不知道這個有沒考,也不是很難:

const obj = {
  a: {
    b: {
      c: 1,
      d: 2,
    },
    e: 3,
  },
  f: {
    g: 4,
    h: {
      i: 5,
    },
  },
};
// 1. 設定結果集
const result = [];

// 2. 遞迴
const recursion = (obj, path = []) => {
  // 2.1 如果到底部,此時 obj 是對應的值
  if (typeof obj !== 'object') {
    // 2.1.1 結果集加上這個欄位
    result.push({
      [path.join('.')]: obj,
    })
    // 2.1.2 終止遞迴
    return;
  }

  // 2.2 遍歷 obj 物件
  for (let i in obj) {

    // 2.2.1 判斷物件自身是否含有該欄位(排除原型鏈)
    if (obj.hasOwnProperty(i)) {

      // 2.2.2 回溯,新增路徑
      path.push(i);

      // 2.2.3 進一步遞迴
      recursion(obj[i], path);

      // 2.2.4 回溯,刪除路徑,方便下一次使用
      path.pop();
    }
  }
};
recursion(obj);

// 3. 返回結果
console.log(result);
/*
[
  { 'a.b.c': 1 },
  { 'a.b.d': 2 },
  { 'a.e': 3 },
  { 'f.g': 4 },
  { 'f.h.i': 5 },
]
*/
複製程式碼

還有反向推題:

  • 根據 obj 和路徑 patha.b.c),找到它的值

遞迴一下就行了,或者迭代也可以,不難。

寫不出來的小哥反省下,寫不出來的小姐姐找我,教你啊~ /手動狗頭防暴力

 陣列去重

看著本文標題是 3 種,實際上有 5 種。

方法一:手撕去重

const jsliangSet = (arr) => {
  // 設定結果
  const result = [];

// 遍歷陣列
for (let i = 0; i < arr.length; i++) {
// 如果結果集不包含這個元素
// 這裡也可以用 result.indexOf(arr[i]) === -1
// 或者 arr.lastIndexOf(arr[i]) === i
if (!result.includes(arr[i])) {
result.push(arr[i]);
}
}

// 返回結果
return result;
};

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

方法二:Set

const jsliangSet = (arr) => {
  return [...new Set(arr)];
};

console.log(jsliangSet([1, 1, 1, 2, 2])); // [1, 2]

方法三:filter

同樣,通過 filter 也可以,其實核心也是 lastIndexOf 和當前索引值的一個比對。

const jsliangSet = (arr) => {
  return arr.filter((item, index) => {
    return arr.lastIndexOf(item) === index;
  })
};

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

其他

其他的還有:

  • 釋出訂閱模式:Node 回撥函式、Vue event bus
  • 非同步併發數限制
  • 非同步序列|非同步並行
  • 圖片懶載入
  • 滾動載入
  • 陣列 API 實現:filtermapforEachreduce
  • 大資料渲染(渲染幾萬條資料不卡頁面)
  • JSON:JSON.parse()JSON.stringify()
作者:jsliang
 

相關文章