《前端面試之道-JS篇》(下)

yvonneit發表於2019-03-28

繼承

原型和原型鏈

原型:每個物件都會在其內部初始化一個屬性,就是prototype(原型)屬性,類似一個指標。

原型鏈:當我們訪問一個物件的屬性時,如果這個物件內部不存在這個屬性,那麼就會去prototype裡找這個屬性,如此遞推下去,一直檢索到 Object 內建物件。

《js高階程式設計》:原型鏈就是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。

原型鏈例子:

function Father(){
	this.property = true;
}
Father.prototype.getFatherValue = function(){
	return this.property;
}
function Son(){
	this.sonProperty = false;
}
//繼承 Father
Son.prototype = new Father();
//Son.prototype被重寫,導致Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
	return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true

//instance例項通過原型鏈找到了Father原型中的getFatherValue方法.
複製程式碼

原型鏈問題:

  • 當原型鏈中包含引用型別值的原型時,該引用型別值會被所有例項共享;
  • 在建立子型別(例如建立Son的例項)時,不能向超型別(例如Father)的建構函式中傳遞引數.

繼承方式推薦

借用建構函式 + 原型鏈 = 組合繼承混合方式

在子類建構函式內部使用apply或者call來呼叫父類的函式即可在實現屬性繼承的同時,又能傳遞引數,又能讓例項不互相影響。

function Super(){
    this.flag = true;
}
Super.prototype.getFlag = function(){
    return this.flag;     //繼承方法
}
function Sub(){
    this.subFlag = flase
    Super.call(this)    //繼承屬性
}
Sub.prototype = new Supe();
Sub.prototype.constructor = Sub;
var obj = new Sub();
Super.prototype.getSubFlag = function(){
    return this.flag;
}
複製程式碼

小問題: Sub.prototype = new Super; 會導致Sub.prototypeconstructor指向Super; 然而constructor的定義是要指向原型屬性對應的建構函式的,Sub.prototypeSub建構函式的原型,所以應該新增一句糾正:Sub.prototype.constructor = Sub;

組合繼承是 JavaScript 最常用的繼承模式,不過它也有不足的地方: 就是無論什麼情況下,都會呼叫兩次父類建構函式: 一次是在建立子型別原型的時候, 另一次是在子型別建構函式內部。

寄生組合式繼承

為了降低呼叫父類建構函式的開銷而出現, 基本思路是不必為了指定子型別的原型而呼叫超型別的建構函式。

function extend(subClass,superClass){
    //建立物件
  var prototype = object(superClass.prototype);
  prototype.constructor = subClass;//增強物件
  subClass.prototype = prototype;//指定物件
}
複製程式碼

extend的高效率體現在它沒有呼叫superClass建構函式,因此避免了在subClass.prototype上面建立不必要,多餘的屬性,同時原型鏈還能保持不變,因此還能正常使用 instanceofisPrototypeOf() 方法.

ES6的class

其內部其實也是ES5組合繼承的方式,通過call借用建構函式,在A類建構函式中呼叫相關屬性,再用原型鏈的連線實現方法的繼承。

class B extends A {
  constructor() {
    return A.call(this);  //繼承屬性
  }
}
A.prototype = new B;  //繼承方法  
複製程式碼

ES6封裝了class,extends關鍵字來實現繼承,內部的實現原理其實依然是基於上面所講的原型鏈,不過進過一層封裝後,Javascript的繼承得以更加簡潔優雅地實現。

class ColorPoint extends Point {
//通過constructor來定義建構函式,用super呼叫父類的屬性方法
  constructor(x, y, color) {
    super(x, y); // 等同於parent.constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // 等同於parent.toString()
  }
}
複製程式碼

可參考:

ES5和ES6中對於繼承的實現方法

JS中的原型和原型鏈(面試中獎率120%)

溫故js系列(15)-原型&原型鏈&原型繼承

JS原型鏈與繼承別再被問倒了

call, apply, bind區別

callapply 都是為了解決改變 this 的指向。

call 方法第一個引數是要繫結給this的值,後面傳入的是一個引數列表。當第一個引數為null、undefined的時候,預設指向window。

apply 接受兩個引數,第一個引數是要繫結給this的值,第二個引數是一個引數陣列。當第一個引數為null、undefined的時候,預設指向window。

var arr1 = [1, 2, 3, 89, 46]
var max = Math.max.call(null, arr1[0], arr1[1], arr1[2], arr1[3], arr1[4])//89

var arr2 = [1,2,3,89,46]
var max = Math.max.apply(null,arr2)//89

//相當於
obj1.fn() === obj1.fn.call/apply(obj1);
fn1() === fn1.call/apply(null)
f1(f2) === f1.call/apply(null,f2)
複製程式碼
//cat give dog
cat.eatFish.call(dog, '汪汪汪', 'call')

//getValue.call(a, 'yck', '24') => a.fn = getValue
let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])
複製程式碼

call 的實現:

Function.prototype.myCall = function (context) {
  var context = context || window
  // 給 context 新增一個屬性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 將 context 後面的引數取出來
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context.fn(...args)
  // 刪除 fn
  delete context.fn
  return result
}
複製程式碼

bind (ES5新增) 和call 很相似,第一個引數是this的指向,從第二個引數開始是接收的引數列表。

區別:bind 方法不會立即執行,而是返回一個改變了上下文 this 後的函式。

可以通過 bind 實現 柯里化。

關於柯里化

柯里化又稱部分求值(Partial Evaluation),簡單來說就是隻傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數。

柯里化有3個常見作用:1. 引數複用;2. 提前返回;3. 延遲計算/執行。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);  // 3
addTen(2);  // 12
複製程式碼

低版本中實現 bind:

if (!Function.prototype.bind) {
    Function.prototype.bind = function () {
        var self = this,                        // 儲存原函式
            context = [].shift.call(arguments), // 儲存需要繫結的this上下文
            args = [].slice.call(arguments);    // 剩餘的引數轉為陣列
        return function () {                    // 返回一個新函式
            self.apply(context, [].concat.call(args, [].slice.call(arguments)));
        }
    }
}
複製程式碼

應用場景:

//1.將類陣列轉化為陣列
var trueArr = Array.prototype.slice.call(arrayLike)

//2.陣列追加
var arr1 = [1,2,3];
var arr2 = [4,5,6];
var total = [].push.apply(arr1, arr2);//6
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]

//3.判斷變數型別
function isArray(obj){
    return Object.prototype.toString.call(obj) == '[object Array]';
}
isArray([]) // true
isArray('dot') // false

//4.利用call和apply做繼承
function Person(name,age){
    // 這裡的this都指向例項
    this.name = name
    this.age = age
    this.sayAge = function(){
        console.log(this.age)
    }
}
function Female(){
    Person.apply(this,arguments)//將父元素所有方法在這裡執行一遍就繼承了
}
var dot = new Female('Dot',2)
複製程式碼

Promise 實現

參考: Promise 是 ES6 新增的語法,解決了回撥地獄的問題。`

回撥地獄:多個回撥函式巢狀

Promise 不是新的語法功能,而是一種新的寫法,允許將回撥函式的巢狀,改成鏈式呼叫

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then
......
複製程式碼

Promise物件的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。

優點:

  1. 有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。
  2. Promise物件提供統一的介面,使得控制非同步操作更加容易。

缺點:

  1. 無法取消Promise,一旦新建它就會立即執行,無法中途取消。
  2. 如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。
  3. 當處於pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

創造 Promise 例項:

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

  if (/* 非同步操作成功 */){
    //resolve 指 fullfilled 狀態
    resolve(value);
  } else {
    reject(error);
  }
});
複製程式碼

Promise例項生成以後,可以用then方法分別指定resolved狀態和rejected狀態的回撥函式。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});
複製程式碼

一個簡單例子:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});
複製程式碼

timeout方法返回一個Promise例項,表示一段時間以後才會發生的結果。過了指定的時間(ms引數)以後,Promise例項的狀態變為resolved,就會觸發then方法繫結的回撥函式。

Generator 實現

形式上,Generator 函式是一個普通函式,但是有兩個特徵。

  1. function關鍵字與函式名之間有一個星號
  2. 函式體內部使用yield表示式,定義不同的內部狀態

呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是遍歷器物件(Iterator Object)。

呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。

// 使用 * 表示這是一個 Generator 函式
// 內部可以通過 yield 暫停程式碼
// 通過呼叫 next 恢復執行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }
複製程式碼

ES6 誕生以前,非同步程式設計的方法,大概有下面四種。

  • 回撥函式
  • 事件監聽
  • 釋出/訂閱
  • Promise 物件 Promise 的最大問題是程式碼冗餘,原來的任務被 Promise 包裝了一下,不管什麼操作,一眼看去都是一堆then,原來的語義變得很不清楚。

因此,Generator 函式是更好的寫法。Generator 函式還可以部署錯誤處理程式碼,捕獲函式體外丟擲的錯誤。

使用 Generator 函式,執行一個真實的非同步任務。

var fetch = require('node-fetch');

function* gen(){
//先讀取一個遠端介面,然後從 JSON 格式的資料解析資訊。
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

//執行
//執行 Generator 函式,獲取遍歷器物件
var g = gen();
//使用next方法執行非同步任務的第一階段
var result = g.next();
//由於Fetch模組返回的是一個 Promise 物件,因此要用then方法呼叫下一個next方法。
result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});
複製程式碼

async 和 await

參考:Async/Await替代Promise的6個理由

ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。

async 函式就是 Generator 函式的語法糖。

async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await。並且返回一個 Promise

async函式對 Generator 函式的改進:

  1. 內建執行器 async函式自帶執行器,不像 Generator 函式,需要呼叫next方法,或者用co模組,才能真正執行。
  2. 更好的語義
  3. 更好的適用性
  4. 返回值是 Promise 進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。

async 、 await 相比直接使用 Promise :

例子:getJSON函式返回一個promise,這個promise成功resolve時會返回一個json物件。我們只是呼叫這個函式,列印返回的JSON物件,然後返回”done”。

// promise
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })
makeRequest()
複製程式碼
//使用Async/Await
const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}
makeRequest()

//async函式會隱式地返回一個promise,該promise的reosolve值就是函式return的值。(示例中reosolve值就是字串”done”)
複製程式碼

優勢: 處理 then 的呼叫鏈能夠更清晰準確的寫出程式碼。

缺點: 濫用 await 可能會導致效能問題,因為 await 會阻塞程式碼,也許之後的非同步程式碼並不依賴於前者,但仍然需要等待前者完成,導致程式碼失去了併發性。

相關文章