重寫JS中的apply,call,bind,new方法

Swiftly發表於2019-01-20

重寫JS中的apply,call,bind,new方法

在js中,經常會用到applycallbindnew,這幾個方法在前端佔據非常重要的作用,今天來看一下這些方法是如何實現,方便更加深入的理解它們的運作原理。

this的繫結問題

引用一點點其他知識點:一個方法的內部上下文this如何確定?

一個方法的呼叫分為一下四種:

  1. 方法直接呼叫,稱之為函式呼叫,當前的上下文this,繫結在全域性的window上,在嚴格模式use strict下,thisnull

  2. 方法作為一個物件的屬性,這個是否通過物件呼叫方法,this繫結在當前物件上。如下:

     let dog = {
     	name: '八公',
     	sayName: function() {
     		console.log(this.name)
     	}
     }
     dog.sayName() // 八公
    複製程式碼
  3. applycall呼叫模式,當前的方法的上下文為方法呼叫的一個入參,如下:

     function sayHello() {
     	console.log(this.hello)
     }
     
     let chineseMan = {
     	hello: '你好啊'
     }
     sayHello.apply(chineseMan) // 你好啊
     
     let englishMan = {
     	hello: 'how are you'
     }
     sayHello.apply(englishMan) // how are you
    複製程式碼
  4. 建構函式的呼叫,當前方法的上下文為新生的例項,如下

     // 宣告建構函式
     function Animal(name) {
     	this.name = name
     	this.sayName = function() {
     		console.log(this.name)
     	}
     }
     
     let dog = new Animal('dog')
     dog.sayName() // dog
     
     let cat = new Animal('cat')
     cat.sayName() // cat
    複製程式碼

正文

apply實現

思路:apply方法實現在Function.prototype中

  1. 獲取到當前呼叫方法體

  2. 獲取方法的入參

  3. 繫結方法體中的上下文為傳入的context--使用的方法就是物件呼叫屬性方法的方式繫結

  4. 呼叫方法

     Function.prototype.myApply = function() {
     	let _fn = this
     	if (typeof _fn !== 'function') {
     		throw new TypeError('error')
     	}
     	let ctx = [...arguments].shift()
     	// 因為apply的入參是陣列,所有隻需要取第一個
     	let args = [...arguments].slice(1).shift()
     	ctx.myApplyFn = _fn
     	// 由於apply會將原方法的引數用陣列包裹一下,所以需要展開引數
     	let res = ctx.myApplyFn(...args)
     	delete ctx.myApplyFn
     	return res
     }
    複製程式碼

call實現

思路:實現在Function.prototype中,大致和apply相似,卻別在對於引數的處理上

  1. 獲取到當前呼叫方法體

  2. 獲取方法的入參

  3. 繫結方法體中的上下文為傳入的context

  4. 呼叫方法

     Function.prototype.myCall = function() {
     	let _fn = this
     	if (typeof _fn !== 'function') {
     		throw new TypeError('error')
     	}
     	let ctx = [...arguments].shift()
     	// call使用的多個入參的方式,所有直接取引數第二個引數開始的所有入參,包裝成一個陣列
     	let args = [...arguments].slice(1)
     	ctx.myCallFn = _fn
     	let res = ctx.myCallFn(...args)
     	delete ctx.myCallFn
     	return res
     }
    複製程式碼

bind實現

思路:實現在Function.prototype中,並且返回一個已經繫結了上下文的函式。利用閉包可以捕獲函式上下文的變數來實現,總體上比起之前兩個方法稍微複雜一些。

  1. 獲取呼叫bind的例項方法體

  2. 獲取需要繫結的上下文context

  3. 宣告閉包函式

  4. 閉包函式中繫結context到例項方法體中

  5. 閉包函式中呼叫原來的方法體

  6. 返回閉包函式

     Function.prototype.myBind = function() {
     	let _fn = this
     	if (typeof _fn !== 'function') {
     		throw new TypeError('error')
     	}
     	let ctx = [...arguments].shift()
     	let args = [...arguments].slice(1)
     	return function() {
     		// 因為bind的呼叫方式,會有bind({}, 'para1', 'para2')('para3', 'para4'),這個時候需要將外面引數和內部引數拼接起來,之後呼叫原來方法
     		args = args.concat([...arguments])
     		ctx.myBindFn = _fn
     		let res = ctx.myBindFn(...args)
     		delete ctx.myBindFn
     		return res
     	}
     }
    複製程式碼

codepen演示 需要翻牆

See the Pen rewrite bind by avg (@beyondverage0908) on CodePen.

new 方法實現

思路:需要明白new到底做了什麼

  1. 生成一個新的例項物件

  2. 例項物件__proto__連結到建構函式的prototype物件

  3. 繫結建構函式的上下文為當前例項

  4. 獲取引數,傳入引數,並呼叫建構函式

     function newObj() {
     	let _o = {}
     	let constructor = [...arguments].shift()
     	let args = [...arguments].slice(1)
     	if (typeof constructor !== 'function') {
     		throw new TypeError('error')
     	}
     	_o.__proto__ = constructor.prototype
     	
     	// 第一種呼叫方式:藉助apply,call,或者bind實現繫結_o
     	// constructor.apply(_o, args)
     	
     	// 第二種,使用屬性方法繫結的方式
     	_o.myNewFn = constructor
     	_o.myNewFn(...args)
     	delete _o.myNewFn
     	return _o
     }
     
     // how to use - 如何使用
     function Animal(name, weight) {
     	this.name = name
     	this.weight = weight
     }
     
     
     let dog = newObj(Animal, 'dog', '18kg')
     // the animal name: dog weight: 18kg
     console.log(`the animal name: ${dog.name} weight: ${dog.weight}`)
     
     let cat = newObj(Animal, 'cat', '11kg')
     // the animal name: cat weight: 11kg
     console.log(`the animal name: ${cat.name} weight: ${cat.weight}`)
    複製程式碼

codepen需要翻牆

See the Pen MZNPoK by avg (@beyondverage0908) on CodePen.

結語

熟悉函式內部的實現,瞭解內部的原理,對理解執行會有很多好處,親自實現一遍會給你很多領悟。同時這些知識點又是非常重要的。

相關文章