前端基礎回顧之手寫題

前言

本文仍是依然針對前端重點基礎知識點進行總體回顧系列的一篇,目標是幫助本身理解避免死記硬背。
下面針對new、Object.create、call、apply、new、bind 等基礎API,從用法到原理實現過一遍,指望看完以後你們實現時不是死記硬背而是根據理解記憶推導。前端

基礎準備

在探究上述內容原理以前,能夠將上述API分爲兩類。
一類是new、Object.create這二者,涉及實例化對象的。es6

其對應的基礎內容部分和上篇前端面試基礎回顧之深刻JS繼承的基礎部分相同。就是原型鏈和構造函數,這裏再也不贅述。面試

剩下的就是關於this指向的修改。
這裏咱們能夠看下MDN中對this的描述
this由調用時環境肯定,簡單總結以下:api

  1. 顯式指定:
  • new 實例化數組

    this指向新構建的對象(new 顯式返回一個對象,則this指向該返回對象,不然指向該對象實例)閉包

    // 例如
    var bar = new foo()
  • bind、call、apply ,指向綁定對象app

    var bar = foo.call( obj2 )
  1. 隱式指定:
  • 函數做爲對象屬性調用,即如object.func()形式,指向該對象。函數

    //指向obj1
     obj1.foo()
  1. 無指定
    即不屬於以上狀況,爲默認綁定。在strict mode下,就是undefined,不然是global對象。post

    var fun1 = obj1.foo
    // this指向全局對象
    fun1()

    這裏順便把this指向也給過了一遍,之後遇到this指向,再複雜的均可以按照這個規律進行判斷。this

既然call、apply、new、bind具有修改this指向的功能,那麼具體如何實現,就是下面要討論的內容。

手寫實現

new

用法

new 用法比較常見,舉個MDN例子:

function Car(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
}

const car1 = new Car('Eagle', 'Talon TSi', 1993);

console.log(car1.make);
// expected output: "Eagle"

這裏實例化了一個Car的實例對象car1,就很少說了。

分析

咱們關注該方法功能是什麼,而後由此推如何手寫實現。
根據MDN的說法:

  1. 一個繼承自 Foo.prototype 的新對象被建立。
  2. 使用指定的參數調用構造函數 Foo,並將 this 綁定到新建立的對象。new Foo 等同於 new Foo(),也就是沒有指定參數列表,Foo 不帶任何參數調用的狀況。
  3. 由構造函數返回的對象就是 new 表達式的結果。若是構造函數沒有顯式返回一個對象,則使用步驟1建立的對象。(通常狀況下,構造函數不返回值,可是用戶能夠選擇主動返回對象,來覆蓋正常的對象建立步驟)

咱們要實現的點主要也有兩個:

  1. 實現一個新對象
  2. 繼承F的屬性
  3. 綁定this

如何實現上述兩點,就用到咱們的基礎知識了。

  1. 新建對象,這個顯然都會。
  2. 繼承F的屬性
    這裏說繼承可能不如說賦值更好理解一些。
    對於一個構造函數來講,屬性包括兩部分實例屬性和原型屬性。
    新對象要繼承其原型屬性,修改原型鏈指向便可。
    繼承實例屬性,將構造函數F的this指向新對象,並執行一次就實現了對新對象的賦值。該過程順便還實現了this的綁定。

結合該思路一塊兒來看看實現思路

實現

第一版實現:

// 1.首先聲明函數my_new
function my_new(func){
    // 2. 新建對象
    var o = {}
    // 3. 修改原型鏈
    o._proto_ = func.prototype
    // 示例屬性獲取,並修改this
    func.call(o)
    // 返回對象
    return o
}

根據分析天然就實現了上面的代碼。

不過new 還有個點分析時上面沒有提到,由構造函數返回的對象就是 new 表達式的結果。若是構造函數沒有顯式返回一個對象,則使用步驟1建立的對象。

假如構造函數返回了對象,那麼須要進行判斷func執行的結果是否是對象,不能直接返回執行結果。

// 1.首先聲明函數my_new
function my_new(func){
    // 2. 新建對象
    var o = {}
    // 3. 修改原型鏈
    o._proto_ = func.prototype
    // 示例屬性獲取,並修改this
    // 獲取構造函數執行結果,判斷是否有顯式返回。
    var res = func.call(o)
    // 視res類型決定返回對象
    return  typeof res === "object" ?res : o
}

到這裏new 的實現就完成了。

Object.create

用法

該方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__。即基於現有對象建立一個新的對象,直接看代碼比較直接:

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction();
// expected output: "My name is Matthew. Am I human? true"

分析

該方法的功能在於兩點:

  1. 一個新對象
  2. 帶有指定的原型對象和屬性

結合上述,倒序來分析:

  1. 帶有指定的原型對象和屬性
    這裏比較特殊,由於原始對象不是構造函數,要繼承其全部屬性的話,仍是要藉助構造函數來實現,即原始對象person,做爲新構造函數F的原型對象,新對象me是F的實例。
  2. 一個新對象
    新的對象能夠是字面量聲明,也能夠經過使用new來實例化。這裏就是後者了。這也是倒序分析的緣由。

    實現

// 1. 聲明函數
function create(Obj){
    // 2. 新建構造函數
    function F() {}
    // 3. 原型鏈修改
    F.prototype = Obj
    // 4.新建對象
    return new F()
}

至於ES6正式規範中仍是能夠第二個參數的狀況暫時不補充,我也沒有見到比較好的實現,你們能夠補充。

call和apply

這二者用法和實現差異不大,就放一塊兒分析了。

用法

採用W3C的例子

//call 用法
var person = {
    firstName:"Steve",
    lastName: "Jobs",
    fullName: function() {
        return this.firstName + " " + this.lastName;
    }
}
var person1 = {
    firstName:"Bill",
    lastName: "Gates",
}
person.fullName.call(person1);  //  "Bill Gates"
// apply 用法
person.fullName.apply(person1);  // 將返回 "Bill Gates"

這裏沒有體現出二者差異,差異在於傳參的不一樣。

  • call() 方法分別接受參數。

  • apply() 方法接受數組形式的參數。

    分析

    call函數的功能有以下幾點:

  • 改變函數中this指向
  • 獲取後續參數則並執行

針對以上兩點,主要在於如何改變this指向。

  • 回顧準備裏面的內容,改變this指向的方法,除去顯式的,咱們也只剩下做爲對象屬性調用了。
    即將函數賦值給被調用對象,做爲其屬性方法執行,至於參數執行時調用就好。

不過這裏有些點要注意

  • 咱們給被調用對象增長屬性,執行完畢以後仍是要刪除的,避免與其餘操做。
  • 一樣增長屬性時,屬性名也要注意避免衝突,最好直接使用Symbol

實現

call的實現:

// 函數
Function.prototype._call = function (ctx) {
    // 1. 構造被調用對象,兼容默認值
    var obj = ctx || window
    // 2. 獲取後續參數
    var args = Array.from(arguments).slice(1)
    // 3. 獲取惟一屬性名
    var fun = Symbol()
    // 4. 增長屬性方法,指向待調用函數
    obj[fun] = this
    var result = obj[fun](...args)
    // 5. 執行完畢後,刪除該屬性
    delete obj[fun]
    return result
}

apply實現與call很相似只是參數處理有些差異。

Function.prototype._apply = function (ctx) {
    var obj = ctx || window
    var args = Array.from(arguments).slice(1)
    var fun = Symbol()
    // 參數處理
    var result = obj[fun](args.join(','))
    delete obj[fun]
    return result
}

bind

用法

MDN描述:bind() 方法建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的參數,供調用時使用。
使用方式以下:

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
}

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());// expected output: 42

分析

其功能分爲以下幾點:

  • 修改this指向到指定對象
  • 返回函數,可被後續執行
  • 參數處理,較簡單

解決思路:

  • this指向
    由於call等的實現,這裏就能夠偷懶了,使用apply來實現
  • 返回一個函數
    常見的閉包形式
  • 參數處理
    可能稍微複雜點的在於,執行時要考慮後續的參數拼接。

實現

第一版實現:

Function.prototype._bind = function(ctx){
    // 1. 兼容判斷
    var ctx = ctx || window
    // 2. 保留當前獲取參數
    var args = Array.from(arguments).slice(1)
    var _this = this
    // 3. 返回函數
    return function F (arguments){
        //   4. 綁定this指向,拼接新增參數
        return _this.apply(ctx,args.concat(arguments))
    }
}

上述完成了第一版的功能要求,可是bind還有一種狀況,返回的畢竟是個函數,就能夠當作構造函數與new 結合使用。

function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype.toString = function () {
    return this.x + ',' + this.y;
};
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0/*x*/);

var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5' 此時this指向當前示例對象,而非emptyObj

這種場景下,爲何this指向了實例對象,主要是new 自己的功能體現。
而咱們的api要支持new 的狀況仍是要結合new 的功能來看。

new 經過調用構造函數,產生了一個示例對象。主要是下面這段代碼。

var res = func.call(o)

結合到咱們的call中,此時func即爲咱們return 的F函數。
即此時函數中的this 爲F的示例,由此能夠區分兩種場景。

Function.prototype._bind = function (ctx) {
    // 1. 兼容判斷
    var ctx = ctx || window
    // 2. 保留當前獲取參數
    var args = Array.from(arguments).slice(1)
    var _this = this
    // 3. 返回函數
    return function F(arguments) {
        // 4.判斷是否new 場景
        if(this instanceof F){
            // 5. 此時直接執行構造函數
            return new _this(...args, ...arguments)
        }else{
            //   5. 常規場景,依然綁定this指向,拼接新增參數
            return _this.apply(ctx, args.concat(arguments))
        }
        
    }
}

結束語

到這裏,幾個簡單的手寫題就總結完畢了,上面的例子多出自MDN。固然上面的代碼都存在一個問題就是對於異常的處理。這裏就不列出了,你們能夠自行補充。對於前面提到的js繼承的基礎,能夠看我前面的文章。仍是一樣一句話共勉你我,你若怒放蝴蝶自來。

相關文章
相關標籤/搜索