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

繼承

原型和原型鏈

原型:每一個對象都會在其內部初始化一個屬性,就是prototype(原型)屬性,相似一個指針。javascript

原型鏈:當咱們訪問一個對象的屬性時,若是這個對象內部不存在這個屬性,那麼就會去prototype裏找這個屬性,如此遞推下去,一直檢索到 Object 內建對象。java

《js高級程序設計》:原型鏈就是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。node

原型鏈例子:git

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方法.
複製代碼

原型鏈問題:es6

  • 當原型鏈中包含引用類型值的原型時,該引用類型值會被全部實例共享;
  • 在建立子類型(例如建立Son的實例)時,不能向超類型(例如Father)的構造函數中傳遞參數.

繼承方式推薦

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

在子類構造函數內部使用apply或者call來調用父類的函數便可在實現屬性繼承的同時,又能傳遞參數,又能讓實例不互相影響。github

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 最經常使用的繼承模式,不過它也有不足的地方: 就是不管什麼狀況下,都會調用兩次父類構造函數: 一次是在建立子類型原型的時候, 另外一次是在子類型構造函數內部。編程

寄生組合式繼承

爲了下降調用父類構造函數的開銷而出現, 基本思路是沒必要爲了指定子類型的原型而調用超類型的構造函數。json

function extend(subClass,superClass){
    //建立對象
  var prototype = object(superClass.prototype);
  prototype.constructor = subClass;//加強對象
  subClass.prototype = prototype;//指定對象
}
複製代碼

extend的高效率體如今它沒有調用superClass構造函數,所以避免了在subClass.prototype上面建立沒必要要,多餘的屬性,同時原型鏈還能保持不變,所以還能正常使用 instanceofisPrototypeOf() 方法.segmentfault

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 會阻塞代碼,也許以後的異步代碼並不依賴於前者,但仍然須要等待前者完成,致使代碼失去了併發性。

相關文章
相關標籤/搜索