前端知識填坑記(二):call和apply,bind ,new

weixin_34402408發表於2018-10-01

前端知識填坑記(一):瀏覽器核心,事件委託

call和apply,bind 的模擬實現

JavaScript 之 call和apply,bind 的模擬實現

call

call()方法在使用一個指定的 this值和若干個指定的引數值的前提下呼叫某個函式或方法。

const foo = {
    value: 1
}
 
function bar() {
    console.log(this.value)
}
 
bar.call(foo) // 1
  • call 改變了 this 的指向,指向到 foo
  • bar 函式執行了
const foo = {
  value: 1,
  bar: function() {
    console.log(this.value)
  }
}

我們模擬的步驟可以分為:

  • 將函式設為物件的屬性
  • 執行該函式
  • 刪除該函式
// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn
第一版:改變作用域
Function.prototype.call = function (con) {
    const context = con || window
    // 首先要呼叫獲取 call 的函式,可以用 this 獲取
    context.fn = this 
    context.fn()
    delete context.fn
  }
  const foo = {
    value: 1
  }

  function bar() {
    console.log(this.value)
  }
  bar.call(foo)
第二版:實現傳參
Function.prototype.call1 = function (con) {
    // 首先要呼叫獲取 call 的函式,可以用 this 獲取
    const context = con || window
    let args = []
    for (let i = 1, len = arguments.length; i < len; i++) {
      args.push('arguments[' + i + ']');
    }
    context.fn = this
    const result = eval('context.fn(' + args + ')')
    delete context.fn
    return result
  }
  const foo = {value: 1}

  function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
  }

  bar.call1(foo, 'kevin', 18)

apply

apply()方法在使用一個指定的 this值和指定的引數陣列的前提下呼叫某個函式或方法。

Function.prototype.apply = function(con, arr) {
    const context = con || window
    context.fn = this
    let result
    if(!arr) {
      result = context.fn()
    } else {
      const args = []
      for(let i = 0,len = arr.length; i < len; i++) {
        args.push(arr[ i ])
      }
      result = eval('context.fn('+ args +')')
    }
    delete context.fn
    return result
  }

bind

bind()方法會建立一個新函式。當這個新函式被呼叫時,bind()的第一個引數將作為它執行時的this,之後的一序列引數將會在傳遞的實參前傳入作為它的引數。

const foo = {
    value: 1
  }
  function bar(name, age) {
    console.log(this.value)
    console.log(name)
    console.log(age)
  }

  let bindFoo = bar.bind(foo, 'daisy')
  bindFoo('18')

函式需要傳nameage 兩個引數,竟然還可以在bind的時候,只傳一個name,在執行返回的函式的時候,再傳另一個引數 age!

Function.prototype.bind1 = function (con) {
    const self = this
    // 獲取bind1函式從第二個引數到最後一個引數
    let args = Array.prototype.slice.call(arguments, 1)

    return function () {
      // 這個時候的arguments是指bind返回的函式傳入的引數
      const bindArgs = Array.prototype.slice.call(arguments)
      s1elf.apply(context, args.concat(bindArgs))
    }
  }
建構函式效果的模擬實現

一個繫結函式也能使用new操作符建立物件:這種行為就像把原函式當成構造器。提供的this值被忽略,同時呼叫時的引數被提供給模擬函式。

bind返回的函式作為建構函式的時候,bind時指定的this值會失效,但傳入的引數依然生效。

const value = 2
  const foo = {
    value: 1
  }

  function bar(name, age) {
    this.habit = 'shopping'
    console.log(this.value);
    console.log(name)
    console.log(age)
  }

  bar.prototype.friend = 'kevin'
  const bindFoo = bar.bind(foo, 'daisy')
  const obj = new bindFoo('18')
  // undefined
  // daisy
  // 18
  console.log(obj.habit)
  console.log(obj.friend)
  // shopping
  // kevin

儘管在全域性和 foo中都宣告瞭value值,最後依然返回了undefind,說明繫結的this失效了,這個時候的this已經指向了obj

Function.prototype.bind2 = function (context) {
    const self = this;
    const args = Array.prototype.slice.call(arguments, 1)
    const fBound = function () {
      const bindArgs = Array.prototype.slice.call(arguments)
      // 當作為建構函式時,this 指向例項,此時結果為 true,將繫結函式的 this 指向該例項,可以讓例項獲得來自繫結函式的值
      // 以上面的是 demo 為例,如果改成 `this instanceof fBound ? null : context`,例項只是一個空物件,將 null 改成 this ,例項會具有 habit 屬性
      // 當作為普通函式時,this 指向 window,此時結果為 false,將繫結函式的 this 指向 context
      self.apply(this instanceof fBound ? this : context, args.concat(bindArgs))
    }
    // 修改返回函式的 prototype 為繫結函式的 prototype,例項就可以繼承繫結函式的原型中的值
    fBound.prototype = this.prototype
    return fBound;
  }
建構函式效果的優化實現

但是在這個寫法中,我們直接將fBound.prototype = this.prototype,我們直接修改 fBound.prototype的時候,也會直接修改繫結函式的prototype。這個時候,我們可以通過一個空函式來進行中轉:

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
    }

    const self = this
    const args = Array.prototype.slice.call(arguments, 1)

    const fNOP = function () {}

    const fBound = function () {
      const bindArgs = Array.prototype.slice.call(arguments)
      self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs))
    }

    fNOP.prototype = this.prototype
    fBound.prototype = new fNOP()
    return fBound
  }

new

JavaScript深入之new的模擬實現

new運算子建立一個使用者定義的物件型別的例項或具有建構函式的內建物件型別之一。

因為new是關鍵字,所以無法像bind 函式一樣直接覆蓋,所以我們寫一個函式,命名為objectFactory,來模擬new的效果。用的時候是這樣的:

第一版

function objectFactory() {

    const obj = new Object()
    const Constructor = [].shift.call(arguments)
    obj.__proto__ = Constructor.prototype
    Constructor.apply(obj, arguments)
    return obj
}

在這一版中,我們:

  • new Object()的方式新建了一個物件 obj
  • 取出第一個引數,就是我們要傳入的建構函式。此外因為shift 會修改原陣列,所以arguments 會被去除第一個引數
  • obj的原型指向建構函式,這樣obj 就可以訪問到建構函式原型中的屬性
  • 使用apply,改變建構函式this 的指向到新建的物件,這樣obj就可以訪問到建構函式中的屬性
  • 返回obj
function Person(name, age) {
    this.name = name
    this.age = age
    this.habit = 'Games'
  }

  Person.prototype.strength = 60
  Person.prototype.sayYourName = function () {
    console.log('I am ' + this.name)
  }

  function objectFactory() {
    const obj = new Object()
    const Constructor = [].shift.call(arguments) // Person
    // arguments 除去了第一個引數(建構函式)後的引數
    obj.__proto__ = Constructor.prototype
    Constructor.apply(obj, arguments)
    return obj;
  }

  const person = objectFactory(Person, 'Kevin', '18')

  console.log(person.name) // Kevin
  console.log(person.habit) // Games
  console.log(person.strength) // 60

  person.sayYourName() // I am Kevin
返回值效果實現

接下來我們再來看一種情況,假如建構函式有返回值,舉個例子:

function Person(name, age) {
    this.strength = 60;
    this.age = age;

    return {
        name: name,
        habit: 'Games'
    }
}

const person = new Person('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // undefined
console.log(person.age) // undefined

在這裡我們是返回了一個物件,假如我們只是返回一個基本型別的值呢?

function Person (name, age) {
    this.strength = 60;
    this.age = age;

    return 'handsome boy';
  }

  const person = new Person('Kevin', '18');

  console.log(person.name) // undefined
  console.log(person.habit) // undefined
  console.log(person.strength) // 60
  console.log(person.age) // 18

第二版

function objectFactory() {

    const obj = new Object()
    const Constructor = [].shift.call(arguments)

    obj.__proto__ = Constructor.prototype
    const ret = Constructor.apply(obj, arguments)

    return typeof ret === 'object' ? ret : obj

  }

前端知識填坑記(三):setTimeout,arguments

相關文章