前面系列-this/apply/call问点

前言

前面系列即为前端面试系列(Front-end interview series), 主要内容是一些前端面试中经常被问到的题.

系列问答中没有繁琐的讲解过程, 力求保证面试者给予面试官一个简洁、具有重点的答案, 所以适合于有一定知识基础的前端童鞋?‍?. 当然, 在每题的最后我也会贴上关于这一章节比较好文章, 以供大家更好的理解所提到的知识点.

请认准github地址: LinDaiDai-FI

一、面试部分

1. 5种this的绑定

  • 默认绑定(非严格模式下this指向全局对象, 严格模式下this会绑定到undefined)
  • 隐式绑定(当函数引用有上下文对象时, 如 obj.foo()的调用方式, foo内的this指向obj)
  • 显示绑定(通过call()或者apply()方法直接指定this的绑定对象, 如foo.call(obj))
  • new绑定
  • 箭头函数绑定(this的指向由外层作用域决定的)

⚠️

隐式丢失

被隐式绑定的函数在特定的情况下会丢失绑定对象, 应用默认绑定, 把this绑定到全局对象或者undefined上:

  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原理、使用场景及模拟实现》

二、笔试部分

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教材》

《霖呆呆你来说说浏览器缓存吧》

《怎样让后台小哥哥快速对接你的前端页面》