本文仍是依然針對前端重點基礎知識點進行總體回顧系列的一篇,目標是幫助本身理解避免死記硬背。
下面針對new、Object.create、call、apply、new、bind 等基礎API,從用法到原理實現過一遍,指望看完以後你們實現時不是死記硬背而是根據理解記憶推導。前端
在探究上述內容原理以前,能夠將上述API分爲兩類。
一類是new、Object.create這二者,涉及實例化對象的。es6
其對應的基礎內容部分和上篇前端面試基礎回顧之深刻JS繼承的基礎部分相同。就是原型鏈和構造函數,這裏再也不贅述。面試
剩下的就是關於this指向的修改。
這裏咱們能夠看下MDN中對this的描述。
this由調用時環境肯定,簡單總結以下:api
new 實例化數組
this指向新構建的對象(new 顯式返回一個對象,則this指向該返回對象,不然指向該對象實例)閉包
// 例如 var bar = new foo()
bind、call、apply ,指向綁定對象app
var bar = foo.call( obj2 )
函數做爲對象屬性調用,即如object.func()形式,指向該對象。函數
//指向obj1 obj1.foo()
無指定
即不屬於以上狀況,爲默認綁定。在strict mode下,就是undefined,不然是global對象。post
var fun1 = obj1.foo // this指向全局對象 fun1()
這裏順便把this指向也給過了一遍,之後遇到this指向,再複雜的均可以按照這個規律進行判斷。this
既然call、apply、new、bind具有修改this指向的功能,那麼具體如何實現,就是下面要討論的內容。
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.首先聲明函數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 的實現就完成了。
該方法建立一個新對象,使用現有的對象來提供新建立的對象的__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"
該方法的功能在於兩點:
結合上述,倒序來分析:
一個新對象
新的對象能夠是字面量聲明,也能夠經過使用new來實例化。這裏就是後者了。這也是倒序分析的緣由。
// 1. 聲明函數 function create(Obj){ // 2. 新建構造函數 function F() {} // 3. 原型鏈修改 F.prototype = Obj // 4.新建對象 return new F() }
至於ES6正式規範中仍是能夠第二個參數的狀況暫時不補充,我也沒有見到比較好的實現,你們能夠補充。
這二者用法和實現差異不大,就放一塊兒分析了。
採用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指向。
不過這裏有些點要注意
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 }
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
其功能分爲以下幾點:
解決思路:
第一版實現:
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繼承的基礎,能夠看我前面的文章。仍是一樣一句話共勉你我,你若怒放蝴蝶自來。