
前言
前面系列即为前端面试系列(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
上:
- 使用另一个变量来给函数取别名:
function foo () {
console.log(this.a)
}
var obj = {
a: 1,
foo: foo
}
var bar = obj.foo; // 使用另一个变量赋值
var a = 2;
bar(); // 2
- 将函数作为参数传递时会被隐式赋值. 回调函数丢失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
解决显示绑定中丢失绑定问题
- 硬绑定, 创建一个包裹函数, 来负责接收参数并返回值
// 硬绑定
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);
-
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来创建对象时发生了什么 🤔️?
- 创建(或者说构造了)一个新对象
- 这个新对象进行
[[prototype]]
连接, 将新对象的原型指向构造函数,这样新对象就可以访问到构造函数原型中的属性 - 改变构造函数
this
的指向为新建的对象,这样新对象就可以访问到构造函数中的属性 - 若是函数没有其它的返回值, 则使用new表达式中的函数调用会自动返回这个新对象
详细指南: 《木易杨前端进阶-JavaScript深入之史上最全--5种this绑定全面解析》
3. apply和call的使用场景
语法:
func.apply(thisArg, [argsArray])
func.call(thisArg, arg1, arg2, ...)
- 合并两个数组(
Array.prototype.push.apply(arr1, arr2)
) - 获取数组中的最大最小值(
Math.max.apply(null, arr)
) - 获取数据类型(
Object.prototype.toString.call(obj)
) - 使类数组对象能够使用数组方法(
Array.prototype.slice.call(domNodes)
或者[].slice.call(domNodes)
) - 调用父构造函数实现继承(
SuperType.call(this)
) - 使用
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合并两个数组时第二个数组长度太大时怎么办 🤔️?
问题原因:
- 我们知道可以使用以下方式来进行两个数组的合并:
Array.prototype.push.apply(arr1, arr2);
// or
Array.prototype.push.call(arr1, ...arr2);
- 同时也知道一个函数能够接收的参数的个数是有限的, 不同引擎的限制不同, 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
会返回一个绑定上下文的函数, 而后两者会直接执行这个函数.
在使用场景上:
- 根据实际的业务情况来改变
this
的指向, 比如解决隐式绑定的函数丢失this
的情况 - 可以结合
Function.prototype.call.bind(Object.prototype.toString)
来获取数据类型(前提是Object.prototype.toString
方法没有被覆盖 - 因为
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函数实现
提示:
- 函数内的
this
表示的就是调用的函数 - 可以将上下文传递进去, 并修改
this
的指向 - 返回一个函数
- 可以传入参数
- 柯里化
- 一个绑定的函数也能使用
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
或者扫一扫下面的二维码👇👇👇.
我会不定时的更新一些前端方面的知识内容以及自己的原创文章🎉
你的鼓励就是我持续创作的动力 😊.

相关推荐:
网友评论