前面系列-this/apply/call問點(假期一塊兒來學習吧, 武漢加油!!!)

前面系列-this/apply/call問點

前言

前面系列即爲前端面試系列(Front-end interview series), 主要內容是一些前端面試中常常被問到的題.html

系列問答中沒有繁瑣的講解過程, 力求保證面試者給予面試官一個簡潔、具備重點的答案, 因此適合於有必定知識基礎的前端童鞋👨‍🎓. 固然, 在每題的最後我也會貼上關於這一章節比較好文章, 以供你們更好的理解所提到的知識點.前端

請認準github地址: LinDaiDai-FInode

1、面試部分

1. 5種this的綁定

  • 默認綁定(非嚴格模式下this指向全局對象, 嚴格模式下 this會綁定到 undefined)
  • 隱式綁定(當函數引用有 上下文對象時, 如 obj.foo()的調用方式, foo內的 this指向 obj)
  • 顯示綁定(經過 call()或者 apply()方法直接指定 this的綁定對象, 如 foo.call(obj))
  • new綁定
  • 箭頭函數綁定( this的指向由外層做用域決定的)

⚠️git

隱式丟失github

被隱式綁定的函數在特定的狀況下會丟失綁定對象, 應用默認綁定, 把this綁定到全局對象或者undefined上:web

  1. 使用另外一個變量來給函數取別名:
function foo () {
	console.log(this.a)
}
var obj = {
	a: 1,
	foo: foo
}
var bar = obj.foo; // 使用另外一個變量賦值
var a = 2;
bar(); // 2
複製代碼
  1. 將函數做爲參數傳遞時會被隱式賦值. 回調函數丟失this綁定是很是常見的:
// 參數傳遞形成的隱式綁定丟失
function foo() {
	console.log(this.a)
}
var obj = {
	a: 1,
	foo: foo // 即便換成 () => foo() 也沒用
}

function doFoo(fn) {
	fn();
}
var a = 2;
doFoo(obj.foo) // 2
複製代碼

解決顯示綁定中丟失綁定問題面試

  1. 硬綁定, 建立一個包裹函數, 來負責接收參數並返回值
// 硬綁定
function foo(params) {
	console.log(this.a, params);
	return this.a + params;
}
var bar = function() {
	return foo.apply(obj, arguments);
}
var obj = {
	a: 1
}
var a = 2;
console.log(bar(3)) // 1, 3; return 4
複製代碼
// 1.簡單的輔助綁定函數
function bind (obj, fn) {
	return function () {
		return fn.apply(obj, arguments);
	}
}

// 2. ES5內置了 Function.prototype.bind 
var bar = foo.bind(obj);
複製代碼
  1. JS中一些內置函數(數組的 forEach、map、filter)提供的可選參數, 能夠指定綁定 this, 其做用和 bind同樣:
// 內置函數提供的可選參數, 指定綁定this
function foo(el) {
	console.log(el, this.a)
}
var obj = {
	a: 'obj a'
};
var a = 'global a';
var arr = [1, 2, 3];
arr.forEach(foo, obj) // 第二個參數爲函數的this指向
// 1 'obj a', 2 'obj a', 3 'obj a'
複製代碼

詳細指南: 《木易楊前端進階-JavaScript深刻之史上最全--5種this綁定全面解析》數組

2. 使用new來建立對象時發生了什麼 🤔️?

  1. 建立(或者說構造了)一個新對象
  2. 這個新對象進行 [[prototype]]鏈接, 將新對象的原型指向構造函數,這樣新對象就能夠訪問到構造函數原型中的屬性
  3. 改變構造函數 this 的指向爲新建的對象,這樣新對象就能夠訪問到構造函數中的屬性
  4. 如果函數沒有其它的返回值, 則使用new表達式中的函數調用會自動返回這個新對象

詳細指南: 《木易楊前端進階-JavaScript深刻之史上最全--5種this綁定全面解析》瀏覽器

3. apply和call的使用場景

語法:緩存

func.apply(thisArg, [argsArray])
func.call(thisArg, arg1, arg2, ...)
複製代碼
  1. 合併兩個數組( Array.prototype.push.apply(arr1, arr2))
  2. 獲取數組中的最大最小值( Math.max.apply(null, arr))
  3. 獲取數據類型( Object.prototype.toString.call(obj))
  4. 使類數組對象可以使用數組方法( Array.prototype.slice.call(domNodes) 或者 [].slice.call(domNodes))
  5. 調用父構造函數實現繼承( SuperType.call(this))
  6. 使用 Object.prototype.hasOwnProperty.call(obj)來檢測 Object.create(null)這種對象

⚠️:

關於第6點:

全部普通對象均可以經過 Object.prototype 的委託來訪問 hasOwnProperty(...),可是對於一些特殊對象( Object.create(null) 建立)沒有鏈接到 Object.prototype,這種狀況必須使用 Object.prototype.hasOwnProperty.call(obj, "a"),顯示綁定到 obj 上。又是一個 call 的用法

例如🌰:

var obj = Object.create(null);
obj.name = 'objName';
console.log(Object.prototype.hasOwnProperty.call(obj5, 'name')); // true
複製代碼

詳細指南: 《木易楊前端進階-深度解析 call 和 apply 原理、使用場景及實現》

4. 使用apply/call合併兩個數組時第二個數組長度太大時怎麼辦 🤔️?

問題緣由:

  1. 咱們知道可使用如下方式來進行兩個數組的合併:
Array.prototype.push.apply(arr1, arr2);
// or
Array.prototype.push.call(arr1, ...arr2);
複製代碼
  1. 同時也知道 一個函數可以接收的參數的個數是有限的, 不一樣引擎的限制不一樣, JS核心限制在65535.

因此爲了解決第二個數組長度太大的問題, 咱們能夠將參數數組切塊後循環傳入目標數組中:

function connectArray (arr1, arr2) {
	const QUANTUM = 32768;
	for (let i = 0, len = arr2.length; i < len; i += QUANTUM) {
		Array.prototype.push.apply(
			arr1,
			arr2.slice(i, Math.min(i + QUANTUM, len))
		)
	}
	return arr1;
}
複製代碼

測試:

var arr1 = [-3, -2, -1];
var arr2 = [];
for (let i = 0; i < 100000; i++) {
  arr2.push(i);
}
connectArray(arr1, arr2);
// arr1.length // 100003
複製代碼

詳細指南: 《木易楊前端進階-深度解析 call 和 apply 原理、使用場景及實現》

5. 如何使用call獲取數據類型 🤔️?

Object.prototype.toString()沒有被修改的狀況下, 咱們能夠用它結合call來獲取數據類型:

[[Class]]是一個內部屬性,值爲一個類型字符串,能夠用來判斷值的類型。

// 手寫一個獲取數據類型的函數
function getClass(obj) {
	let typeString = Object.prototype.toString.call(obj); // "[object Array]"
	return typeString.slice(8, -1);
}
console.log(getClass(new Date)) // Date
console.log(getClass(new Map)) // Map
console.log(getClass(new Set)) // Set
console.log(getClass(new String)) // String
console.log(getClass(new Number)) // Number
console.log(getClass(NaN)) // Number
console.log(getClass(null)) // Null
console.log(getClass(undefined)) // Undefined
console.log(getClass(Symbol(42))) // Symbol
console.log(getClass({})) // Object
console.log(getClass([])) // Array
console.log(getClass(function() {})) // Function
console.log(getClass(document.getElementsByTagName('p'))) // HTMLCollection

console.log(getClass(arguments)) // Arguments
複製代碼

6. 有哪些使類數組對象轉對象的方法 🤔️?

Array.prototype.slice.call(arguments);
// 等同於 [].slice.call(arguments);

ES6:
let arr = Array.from(arguments);
let arr = [...arguments];
複製代碼

Array.from() 能夠將兩類對象轉爲真正的數組:類數組對象和可遍歷(iterable)對象(包括ES6新增的數據結構 Set 和 Map), 好比:

var map1 = new Map();
map1.set("key1", "value1")
map1.set("key2", "value2")
var mapArr = Array.from(map1)
console.log(map1) // Map
console.log(mapArr) // [["key1", "value1"], ["key2", "value2"]] 二維數組
複製代碼

擴展一: 爲何經過 Array.prototype.slice.call() 就能夠把類數組對象轉換成數組 🤔️?

: 由於slice 將類數組對象經過下標操做放入了新的數組中

擴展二: 經過 Array.prototype.slice.call() 就足夠了嗎?存在什麼問題 🤔️?

: 在低版本的IE下不支持Array.prototype.slice.call(args)這種寫法, 由於低版本IE(IE < 9)下的DOM對象是以 com 對象的形式實現的,js對象與 com 對象不能進行轉換。

兼容的寫法爲:

function toArray (nodes) {
  try {
    return Array.prototype.slice.call(nodes);
  } catch (err) {
    var arr = [],
        len = nodes.length;
    for (var i = 0; i < len; i++) {
      arr.push(nodes[i]);
    }
    return arr;
  }
}
複製代碼

擴展三: 爲何要有類數組對象呢?或者說類數組對象是爲何解決什麼問題纔出現的 🤔️?

一句話就是能夠更快的操做複雜數據, 好比音頻視頻編輯, 訪問webSockets的原始數據等.

7. bind的使用場景

語法:

func.bind(thisArg, arg1, arg2, ...)
複製代碼

咱們知道, bind()方法的做用是會建立一個新函數, 在這個新函數被調用時, 函數內的this指向bind()的第一個參數, 而其他的參數將做爲新函數的參數被它使用.

因此它與apply/call最大的區別是bind會返回一個綁定上下文的函數, 然後二者會直接執行這個函數.

在使用場景上:

  1. 根據實際的業務狀況來改變 this的指向, 好比解決隱式綁定的函數丟失 this的狀況
  2. 能夠結合 Function.prototype.call.bind(Object.prototype.toString)來獲取數據類型(前提是 Object.prototype.toString 方法沒有被覆蓋
  3. 由於 bind是會返回一個新函數的, 因此咱們還能夠用它來實現柯里化, bind自己也是閉包的一種使用場景.

詳細指南: 《木易楊前端進階-深度解析bind原理、使用場景及模擬實現》

2、筆試部分

1. this指向問題

/** * 非嚴格模式 */
var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()
複製代碼






答案:

person1.show1() // person1 隱式綁定, this指向調用者
person1.show1.call(person2) // person2 顯示綁定, this指向person2

person1.show2() // window,箭頭函數綁定,this指向外層做用域,即全局做用域
person1.show2.call(person2) // window, 使用call硬綁定person2也沒用,this指向外層做用域,即全局做用域

person1.show3()() // window, 默認綁定, 此函數爲高階函數, 調用者是window
									// 能夠理解爲隱性丟失,使用另外一個變量來給函數取別名: var bar = person1.show3();

person1.show3().call(person2)// person2, 顯式綁定, 將 `var bar = person1.show3()` 這個函數的this 指向 person2

person1.show3.call(person2)() // window, 默認綁定, 雖然將第一層函數內的this指向了person2, 可是內層函數 `var bar = person1.show3()` 的調用者仍是window

person1.show4()() // person1, 第一層函數的this是person1, 內層爲箭頭函數, 指向外層做用域person1
person1.show4().call(person2) // person1, 第一層函數的this是person1, 內層爲箭頭函數,使用call硬綁定person2也沒用,this仍是指向外層做用域person1

person1.show4.call(person2)() // person2, 改變了第一層函數的this指向, 將其指向爲person2, 而內層爲箭頭函數, 指向外層做用域person2
複製代碼

換一種方式: 使用構造函數來建立對象, 並執行4個相同的show方法:

提示: 使用new操做符建立的對象和直接var產生的對象的區別在於:

使用new操做符會產生新的構造函數做用域, 這樣箭頭函數內的this指向的就是這個函數做用域, 而非全局

var name = 'window'

function Person (name) {
  this.name = name;
  this.show1 = function () {
    console.log(this.name)
  }
  this.show2 = () => console.log(this.name)
  this.show3 = function () {
    return function () {
      console.log(this.name)
    }
  }
  this.show4 = function () {
    return () => console.log(this.name)
  }
}

var personA = new Person('personA')
var personB = new Person('personB')

personA.show1()
personA.show1.call(personB)

personA.show2()
personA.show2.call(personB)

personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()

personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()
複製代碼






答案:

personA.show1() // personA,隱式綁定,調用者是 personA
personA.show1.call(personB) // personB,顯式綁定,調用者是 personB

personA.show2() // personA, 與第一題的區別, 此時this指向的是外層做用域 personA函數的做用域
personA.show2.call(personB) // personA, 箭頭函數使用call硬綁定也沒用

personA.show3()() // window, 默認綁定, 調用者是window, 同第一題同樣
personA.show3().call(personB) // personB, 顯示綁定
personA.show3.call(personB)() // window, 默認綁定,調用者是window, 同第一題同樣

personA.show4()() // personA, 箭頭函數綁定,this指向外層做用域,即personA函數做用域
personA.show4().call(personB) // personA, 箭頭函數綁定,call並無改變外層做用域,
personA.show4.call(personB)() // personB, 將第一層函數的this指向改爲了personB, 此時做用域指向personB, 內存函數爲箭頭函數, this指向外層做用域,即personB函數做用域
複製代碼

2. 手寫一個new實現

function create () {
	var obj = new Object(),
      Con = [].shift.call(arguments);
  obj.__proto__ = Con.prototype;
  var ret = Con.apply(obj, arguments);
  return ret instanceof Object ? ret : obj;
}
複製代碼






過程分析:

function create () {
  // 1. 建立一個新的對象
	var obj = new Object(),
  // 2. 取出第一個參數, 就是咱們要傳入的構造函數; 同時arguments會被去除第一個參數
      Con = [].shift.call(arguments);
  // 3. 將 obj的原型指向構造函數,這樣obj就能夠訪問到構造函數原型中的屬性
  obj.__proto__ = Con.prototype;
  // 4. 使用apply,改變構造函數this 的指向到新建的對象,這樣 obj就能夠訪問到構造函數中的屬性
  var ret = Con.apply(obj, arguments);
  // 5. 優先返回構造函數返回的對象
  return ret instanceof Object ? ret : obj;
}
複製代碼

詳細指南: 《木易楊前端進階-深度解析 new 原理及模擬實現》

3. 手寫一個call函數實現

ES3寫法:

// 建立一個獨一無二的 fn 函數名
function fnFactory(context) {
    var unique_fn = 'fn';
    while (context.hasOwnProperty(unique_fn)) {
        unique_fn = "fn" + Math.random();
    }
    return unique_fn;
}
Function.prototype.call2 = function (context) {
  context = context ? Object(context) : window;
  var args = [];
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
  }
  var fn = fnFactory(context)
  context[fn] = this;
  var result = eval('context[fn](' + args + ')');
  delete context[fn];
  return result;
}
複製代碼

ES6寫法:

Function.prototype.call3 = function (context) {
	context = context ? Object(context) : window;
	var fn = Symbol();
	context[fn] = this;
	
	let args = [...arguments].slice(1);
	let result = context[fn](...args);
	
	delete context[fn];
	return result;
}
複製代碼






過程分析:

// 建立一個獨一無二的 fn 函數名
function fnFactory(context) {
    var unique_fn = 'fn';
    while (context.hasOwnProperty(unique_fn)) {
        unique_fn = "fn" + Math.random();
    }
    return unique_fn;
}
Function.prototype.call2 = function(context) {
    // 1. 如果傳入的context是null或者undefined時指向window;
    // 2. 如果傳入的是原始數據類型, 原生的call會調用 Object() 轉換
    context = context ? Object(context) : window;
  	// 3. 建立一個獨一無二的fn函數的命名
   	var fn = fnFactory(context);
  	// 4. 這裏的this就是指調用call的那個函數
  	// 5. 將調用的這個函數賦值到context中, 這樣以後執行context.fn的時候, fn裏的this就是指向context了
    context[fn] = this;
    // 6. 定義一個數組用於放arguments的每一項的字符串: ['agruments[1]', 'arguments[2]']
    var args = [];
    // 7. 要從第1項開始, 第0項是context
    for (var i = 1, l = arguments.length; i < l; i++) {
        args.push('arguments[' + i + ']')
    }
    // 8. 使用eval()來執行fn並將args一個個傳遞進去
    var result = eval('context[fn](' + args + ')');
    // 9. 給context額外附件了一個屬性fn, 因此用完以後須要刪除
    delete context[fn];
    // 10. 函數fn可能會有返回值, 須要將其返回
    return result;
}
複製代碼

測試代碼:

var obj = {
    name: 'objName'
}

function consoleInfo(sex, weight) {
    console.log(this.name, sex, weight)
}
var name = 'globalName';
consoleInfo.call2(obj, 'man', 100); // 'objName' 'man' 100
consoleInfo.call3(obj, 'woman', 120); // 'objName' 'woman' 120
複製代碼

4. 手寫一個apply函數實現

ES3:

// 建立一個獨一無二的 fn 函數名
function fnFactory (context) {
  var unique_fn = 'fn';
  while (context.hasOwnProperty(unique_fn)) {
    unique_fn = 'fn' + Math.random();
  }
  return unique_fn;
}
Function.prototype.apply2 = function (context, arr) {
	context = context ? Object(context) : window;
	var fn = fnFactory(context);
	context[fn] = this;
	
	var result;
	if (!arr) {
		result = context[fn]();
	} else {
		var args = [];
		for (var i = 0, len = arr.length; i < len; i++) {
			args.push('arr[' + i + ']');
		}
		result = eval('context[fn](' + args + ')');
	}
	delete context[fn];
	return result;
}
複製代碼

ES6:

Function.prototype.apply3 = function (context, arr) {
	context = context ? Object(context) : window;
	let fn = Symbol();
  context[fn] = this;
  
  let result = arr ? context[fn](...arr) : context[fn]();
  delete context[fn];
  return result;
}
複製代碼






過程分析:

// 建立一個獨一無二的 fn 函數名
function fnFactory (context) {
  var unique_fn = 'fn';
  while (context.hasOwnProperty(unique_fn)) {
    unique_fn = 'fn' + Math.random();
  }
  return unique_fn;
}
Function.prototype.apply2 = function (context, arr) {
  // 1. 如果傳入的context是null或者undefined時指向window;
  // 2. 如果傳入的是原始數據類型, 原生的call會調用 Object() 轉換
	context = context ? Object(context) : window;
  // 3. 建立一個獨一無二的fn函數的命名
	var fn = fnFactory(context);
  // 4. 這裏的this就是指調用call的那個函數
  // 5. 將調用的這個函數賦值到context中, 這樣以後執行context.fn的時候, fn裏的this就是指向context了
	context[fn] = this;
	
	var result;
  // 6. 判斷有沒有第二個參數
	if (!arr) {
		result = context[fn]();
	} else {
    // 7. 有的話則用args放每一項的字符串: ['arr[0]', 'arr[1]']
		var args = [];
		for (var i = 0, len = arr.length; i < len; i++) {
			args.push('arr[' + i + ']');
		}
    // 8. 使用eval()來執行fn並將args一個個傳遞進去
		result = eval('context[fn](' + args + ')');
	}
  // 9. 給context額外附件了一個屬性fn, 因此用完以後須要刪除
	delete context[fn];
  // 10. 函數fn可能會有返回值, 須要將其返回
	return result;
}
複製代碼

5. 手寫一個bind函數實現

提示:

  1. 函數內的 this表示的就是調用的函數
  2. 能夠將上下文傳遞進去, 並修改 this的指向
  3. 返回一個函數
  4. 能夠傳入參數
  5. 柯里化
  6. 一個綁定的函數也能使用 new操做法建立對象, 且提供的 this會被忽略
Function.prototype.bind2 = function (context) {
	if (typeof this !== "function") {
		throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
	}
	var self = this;
	var args = Array.prototype.slice.call(arguments, 1);
  
	var fBound = function () {
		var innerArgs = Array.prototype.slice.call(arguments);
		return self.apply(
			this instanceof fNOP ? this : context,
			args.concat(innerArgs)
		)
	}
  
	var fNOP = function () {};
	fNOP.prototype = this.prototype;
	fBound.prototype = new fNOP();
	return fBound;
}
複製代碼






Function.prototype.bind2 = function(context) {
    // 1. 判斷調用bind的是否是一個函數
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
    }
    // 2. 外層的this指向調用者(也就是調用的函數)
    var self = this;
    // 3. 收集調用bind時的其它參數
    var args = Array.prototype.slice.call(arguments, 1);
    
    // 4. 建立一個返回的函數
    var fBound = function() {
        // 6. 收集調用新的函數時傳入的其它參數
        var innerArgs = Array.prototype.slice.call(arguments);
        // 7. 使用apply改變調用函數時this的指向
        // 做爲構造函數調用時this表示的是新產生的對象, 不做爲構造函數用的時候傳遞context
        return self.apply(
            this instanceof fNOP ? this : context,
            args.concat(innerArgs)
        )
    }
    // 5. 建立一個空的函數, 且將原型指向調用者的原型(爲了能用調用者原型中的屬性)
    // 下面三步的做用有點相似於 fBoun.prototype = this.prototype 但有區別
    var fNOP = function() {};
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    // 8. 返回最後的結果
    return fBound;
}
複製代碼

後語

喜歡霖呆呆的小夥還但願能夠關注霖呆呆的公衆號👇👇👇.

我會不定時的更新一些前端方面的知識內容以及本身的原創文章🎉

你的鼓勵就是我持續創做的動力 😊.

LinDaiDai公衆號二維碼.jpg

相關推薦:

《JavaScript進階-執行上下文(理解執行上下文一篇就夠了)》

《全網最詳bpmn.js教材》

《霖呆呆你來講說瀏覽器緩存吧》

《怎樣讓後臺小哥哥快速對接你的前端頁面》

相關文章
相關標籤/搜索