前面系列-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教材》

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

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

相关文章
相关标签/搜索