一年前,也许你搞清楚闭包,this,原型链,就能得到承认。可是如今,很显然是不行了。本文梳理出了一些面试中有必定难度的高频原生JS问题,部分知识点可能你以前从未关注过,或者看到了,却没有仔细研究,可是它们却很是重要。javascript
本文将以真实的面试题的形式来呈现知识点,你们在阅读时,建议不要先看个人答案,而是本身先思考一番。尽管,本文全部的答案,都是我在翻阅各类资料,思考并验证以后,才给出的(绝非复制粘贴而来)。但因水平有限,本人的答案未必是最优的,若是您有更好的答案,欢迎给我留言。html
本文篇幅较长,可是满满的都是干货!而且还埋伏了可爱的表情包,但愿小伙伴们可以坚持读完。前端
衷心的祝愿你们都能找到心仪的工做。java
更多文章可戳: github.com/YvetteLau/B…node
首先 typeof 可以正确的判断基本数据类型,可是除了 null, typeof null输出的是对象。git
可是对象来讲,typeof 不能正确的判断其类型, typeof 一个函数能够输出 'function',而除此以外,输出的全是 object,这种状况下,咱们没法准确的知道对象的类型。es6
instanceof能够准确的判断复杂数据类型,可是不能正确判断基本数据类型。(正确判断数据类型请戳:github.com/YvetteLau/B…)github
instanceof 是经过原型链判断的,A instanceof B, 在A的原型链中层层查找,是否有原型等于B.prototype,若是一直找到A的原型链的顶端(null;即Object.proptotype.__proto__
),仍然不等于B.prototype,那么返回false,不然返回true.web
instanceof的实现代码:面试
// L instanceof R
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显式原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null) //已经找到顶层
return false;
if (O === L) //当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__; //继续向上一层原型链查找
}
}
复制代码
PS: Object.keys():返回给定对象全部可枚举属性的字符串数组。
关于forEach是否会改变原数组的问题,有些小伙伴提出了异议,为此我写了代码测试了下(注意数组项是复杂数据类型的状况)。 除了forEach以外,map等API,也有一样的问题。
let arry = [1, 2, 3, 4];
arry.forEach((item) => {
item *= 10;
});
console.log(arry); //[1, 2, 3, 4]
arry.forEach((item) => {
arry[1] = 10; //直接操做数组
});
console.log(arry); //[ 1, 10, 3, 4 ]
let arry2 = [
{ name: "Yve" },
{ age: 20 }
];
arry2.forEach((item) => {
item.name = 10;
});
console.log(arry2);//[ { name: 10 }, { age: 20, name: 10 } ]
复制代码
如还不了解 iterator 接口或 for...of, 请先阅读ES6文档: Iterator 和 for...of 循环
更多细节请戳: github.com/YvetteLau/B…
arr.constructor === Array
. (不许确,由于咱们能够指定 obj.constructor = Array
)function fn() {
console.log(Array.isArray(arguments)); //false; 由于arguments是类数组,但不是数组
console.log(Array.isArray([1,2,3,4])); //true
console.log(arguments instanceof Array); //fasle
console.log([1,2,3,4] instanceof Array); //true
console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array]
console.log(arguments.constructor === Array); //false
arguments.constructor = Array;
console.log(arguments.constructor === Array); //true
console.log(Array.isArray(arguments)); //false
}
fn(1,2,3,4);
复制代码
类数组:
1)拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当作字符串来处理);
2)不具备数组所具备的方法;
类数组是一个普通对象,而真实的数组是Array类型。
常见的类数组有: 函数的参数 arguments, DOM 对象列表(好比经过 document.querySelectorAll 获得的列表), jQuery 对象 (好比 $("div")).
类数组能够转换为数组:
//第一种方法
Array.prototype.slice.call(arrayLike, start);
//第二种方法
[...arrayLike];
//第三种方法:
Array.from(arrayLike);
复制代码
PS: 任何定义了遍历器(Iterator)接口的对象,均可以用扩展运算符转为真正的数组。
Array.from方法用于将两类对象转为真正的数组:相似数组的对象(array-like object)和可遍历(iterable)的对象。
=== 不须要进行类型转换,只有类型相同而且值相等时,才返回 true.
== 若是二者类型不一样,首先须要进行类型转换。具体流程以下:
let person1 = {
age: 25
}
let person2 = person1;
person2.gae = 20;
console.log(person1 === person2); //true,注意复杂数据类型,比较的是引用地址
复制代码
[] == ![]
咱们来分析一下: [] == ![]
是true仍是false?
![]
引用类型转换成布尔值都是true,所以![]
的是false修改原数组的API有:
splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift
不修改原数组的API有:
slice/map/forEach/every/filter/reduce/entries/find
注: 数组的每一项是简单数据类型,且未直接操做数组的状况下(稍后会对此题从新做答)。
变量提高就是变量在声明以前就可使用,值为undefined。
在代码块内,使用 let/const 命令声明变量以前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。暂时性死区也意味着 typeof 再也不是一个百分百安全的操做。
typeof x; // ReferenceError(暂时性死区,抛错)
let x;
复制代码
typeof y; // 值是undefined,不会报错
复制代码
暂时性死区的本质就是,只要一进入当前做用域,所要使用的变量就已经存在了,可是不可获取,只有等到声明变量的那一行代码出现,才能够获取和使用该变量。
this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定.
测试下是否已经成功Get了此知识点(浏览器执行环境):
var number = 5;
var obj = {
number: 3,
fn1: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);
复制代码
若是this的知识点,您还不太懂,请戳: 嗨,你真的懂this吗?
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境, JS执行上下文栈能够认为是一个存储函数调用的栈结构,遵循先进后出的原则。
做用域链: 不管是 LHS 仍是 RHS 查询,都会在当前的做用域开始查找,若是没有找到,就会向上级做用域继续查找目标标识符,每次上升一个做用域,一直到全局做用域为止。
题难不难?不难!继续挑战一下!难!知道难,就更要继续了!
闭包是指有权访问另外一个函数做用域中的变量的函数,建立闭包最经常使用的方式就是在一个函数内部建立另外一个函数。
闭包的做用有:
call 和 apply 的功能相同,区别在于传参的方式不同:
fn.call(obj, arg1, arg2, ...),调用一个函数, 具备一个指定的this值和分别地提供的参数(参数的列表)。
fn.apply(obj, [argsArray]),调用一个函数,具备一个指定的this值,以及做为一个数组(或类数组对象)提供的参数。
call核心:
Function.prototype.call = function (context) {
/** 若是第一个参数传入的是 null 或者是 undefined, 那么指向this指向 window/global */
/** 若是第一个参数传入的不是null或者是undefined, 那么必须是一个对象 */
if (!context) {
//context为null或者是undefined
context = typeof window === 'undefined' ? global : window;
}
context.fn = this; //this指向的是当前的函数(Function的实例)
let rest = [...arguments].slice(1);//获取除了this指向对象之外的参数, 空数组slice后返回的仍然是空数组
let result = context.fn(...rest); //隐式绑定,当前函数的this指向了context.
delete context.fn;
return result;
}
//测试代码
var foo = {
name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
console.log(this.name);
console.log(job, age);
}
bar.call(foo, 'programmer', 20);
// Selina programmer 20
bar.call(null, 'teacher', 25);
// 浏览器环境: Chirs teacher 25; node 环境: undefined teacher 25
复制代码
apply:
apply的实现和call很相似,可是须要注意他们的参数是不同的,apply的第二个参数是数组或类数组.
Function.prototype.apply = function (context, rest) {
if (!context) {
//context为null或者是undefined时,设置默认值
context = typeof window === 'undefined' ? global : window;
}
context.fn = this;
let result;
if(rest === undefined || rest === null) {
//undefined 或者 是 null 不是 Iterator 对象,不能被 ...
result = context.fn(rest);
}else if(typeof rest === 'object') {
result = context.fn(...rest);
}
delete context.fn;
return result;
}
var foo = {
name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
console.log(this.name);
console.log(job, age);
}
bar.apply(foo, ['programmer', 20]);
// Selina programmer 20
bar.apply(null, ['teacher', 25]);
// 浏览器环境: Chirs programmer 20; node 环境: undefined teacher 25
复制代码
bind
bind 和 call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,可是 bind 会建立一个新函数。当这个新函数被调用时,bind() 的第一个参数将做为它运行时的 this,以后的一序列参数将会在传递的实参前传入做为它的参数。
Function.prototype.bind = function(context) {
if(typeof this !== "function"){
throw new TypeError("not a function");
}
let self = this;
let args = [...arguments].slice(1);
function Fn() {};
Fn.prototype = this.prototype;
let bound = function() {
let res = [...args, ...arguments]; //bind传递的参数和函数调用时传递的参数拼接
context = this instanceof Fn ? this : context || this;
return self.apply(context, res);
}
//原型链
bound.prototype = new Fn();
return bound;
}
var name = 'Jack';
function person(age, job, gender){
console.log(this.name , age, job, gender);
}
var Yve = {name : 'Yvette'};
let result = person.bind(Yve, 22, 'enginner')('female');
复制代码
new:
function new(func) {
let target = {};
target.__proto__ = func.prototype;
let res = func.call(target);
if (res && typeof(res) == "object" || typeof(res) == "function") {
return res;
}
return target;
}
复制代码
字面量建立对象,不会调用 Object构造函数, 简洁且性能更好;
new Object() 方式建立对象本质上是方法调用,涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的 堆栈信息,方法调用结束后,还要释放该堆栈,性能不如字面量的方式。
经过对象字面量定义对象时,不会调用Object构造函数。
在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预约义的属性。其中每一个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。使用原型对象的好处是全部对象实例共享它所包含的属性和方法。
原型链解决的主要是继承问题。
每一个对象拥有一个原型对象,经过 __proto__
(读音: dunder proto) 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null(Object.prototype.__proto__
指向的是null)。这种关系被称为原型链 (prototype chain),经过原型链一个对象能够拥有定义在其余对象中的属性和方法。
构造函数 Parent、Parent.prototype 和 实例 p 的关系以下:(p.__proto__ === Parent.prototype)
__proto__
区别是什么?prototype是构造函数的属性。
__proto__
是每一个实例都有的属性,能够访问 [[prototype]] 属性。
实例的__proto__
与其构造函数的prototype指向的是同一个对象。
function Student(name) {
this.name = name;
}
Student.prototype.setAge = function(){
this.age=20;
}
let Jack = new Student('jack');
console.log(Jack.__proto__);
//console.log(Object.getPrototypeOf(Jack));;
console.log(Student.prototype);
console.log(Jack.__proto__ === Student.prototype);//true
复制代码
组合继承(最经常使用的继承方式)
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}
复制代码
其它继承方式实现,能够参考《JavaScript高级程序设计》
浅拷贝是指只复制第一层对象,可是当对象的属性是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。
深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是彻底隔离的,互不影响,对一个对象的修改并不会影响另外一个对象。
实现一个深拷贝:
function deepClone(obj) { //递归拷贝
if(obj === null) return null; //null 的状况
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
if(typeof obj !== 'object') {
//若是不是复杂数据类型,直接返回
return obj;
}
/** * 若是obj是数组,那么 obj.constructor 是 [Function: Array] * 若是obj是对象,那么 obj.constructor 是 [Function: Object] */
let t = new obj.constructor();
for(let key in obj) {
//若是 obj[key] 是复杂数据类型,递归
t[key] = deepClone(obj[key]);
}
return t;
}
复制代码
看不下去了?别人的送分题会成为你的送命题
防抖和节流的做用都是防止函数屡次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的状况下只会调用一次,而节流的状况会每隔必定时间调用一次函数。
防抖(debounce): n秒内函数只会执行一次,若是n秒内高频事件再次被触发,则从新计算时间
function debounce(func, wait, immediate = true) {
let timer;
// 延迟执行函数
const later = (context, args) => setTimeout(() => {
timer = null;// 倒计时结束
if (!immediate) {
func.apply(context, args);
//执行回调
context = args = null;
}
}, wait);
let debounced = function (...params) {
let context = this;
let args = params;
if (!timer) {
timer = later(context, args);
if (immediate) {
//当即执行
func.apply(context, args);
}
} else {
clearTimeout(timer);
//函数在每一个等待时延的结束被调用
timer = later(context, args);
}
}
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
};
return debounced;
};
复制代码
防抖的应用场景:
节流(throttle): 高频事件在规定时间内只会执行一次,执行一次后,只有大于设定的执行周期后才会执行第二次。
//underscore.js
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function () {
previous = options.leading === false ? 0 : Date.now() || new Date().getTime();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function () {
var now = Date.now() || new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function () {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
复制代码
函数节流的应用场景有:
// ES5 的写法
Math.max.apply(null, [14, 3, 77, 30]);
// ES6 的写法
Math.max(...[14, 3, 77, 30]);
// reduce
[14,3,77,30].reduce((accumulator, currentValue)=>{
return accumulator = accumulator > currentValue ? accumulator : currentValue
});
复制代码
setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等好久,因此并没办法保证回调函数必定会在 setTimeout() 指定的时间执行。因此, setTimeout() 的第二个参数表示的是最少时间,并不是是确切时间。
HTML5标准规定了 setTimeout() 的第二个参数的最小值不得小于4毫秒,若是低于这个值,则默认是4毫秒。在此以前。老版本的浏览器都将最短期设为10毫秒。另外,对于那些DOM的变更(尤为是涉及页面从新渲染的部分),一般是间隔16毫秒执行。这时使用 requestAnimationFrame() 的效果要好于 setTimeout();
0.1 + 0.2 != 0.3 是由于在进制转换和进阶运算的过程当中出现精度损失。
下面是详细解释:
JavaScript使用 Number 类型表示数字(整数和浮点数),使用64位表示一个数字。
图片说明:
计算机没法直接对十进制的数字进行运算, 须要先对照 IEEE 754 规范转换成二进制,而后对阶运算。
1.进制转换
0.1和0.2转换成二进制后会无限循环
0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)
复制代码
可是因为IEEE 754尾数位数限制,须要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。
2.对阶运算
因为指数位数不相同,运算时须要对阶运算 这部分也可能产生精度损失。
按照上面两步运算(包括两步的精度损失),最后的结果是
0.0100110011001100110011001100110011001100110011001100
结果转换成十进制以后就是 0.30000000000000004。
promise有三种状态: fulfilled, rejected, pending.
Promise 的优势:
Promise 的缺点:
Promise的构造函数是同步执行的。then 中的方法是异步执行的。
promise的then实现,详见: Promise源码实现
Promise 是微任务,setTimeout 是宏任务,同一个事件循环中,promise.then老是先于 setTimeout 执行。
要实现 Promise.all,首先咱们须要知道 Promise.all 的功能:
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
let index = 0;
let result = [];
if (promises.length === 0) {
resolve(result);
} else {
function processValue(i, data) {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}
for (let i = 0; i < promises.length; i++) {
//promises[i] 多是普通值
Promise.resolve(promises[i]).then((data) => {
processValue(i, data);
}, (err) => {
reject(err);
return;
});
}
}
});
}
复制代码
若是想了解更多Promise的源码实现,能够参考个人另外一篇文章:Promise的源码实现(完美符合Promise/A+规范)
无论成功仍是失败,都会走到finally中,而且finally以后,还能够继续then。而且会将值原封不动的传递给后面的then.
Promise.prototype.finally = function (callback) {
return this.then((value) => {
return Promise.resolve(callback()).then(() => {
return value;
});
}, (err) => {
return Promise.resolve(callback()).then(() => {
throw err;
});
});
}
复制代码
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术。
function sum(a) {
return function(b) {
return function(c) {
return a+b+c;
}
}
}
console.log(sum(1)(2)(3)); // 6
复制代码
引伸:实现一个curry函数,将普通函数进行柯里化:
function curry(fn, args = []) {
return function(){
let rest = [...args, ...arguments];
if (rest.length < fn.length) {
return curry.call(this,fn,rest);
}else{
return fn.apply(this,rest);
}
}
}
//test
function sum(a,b,c) {
return a+b+c;
}
let sumFn = curry(sum);
console.log(sumFn(1)(2)(3)); //6
console.log(sumFn(1)(2, 3)); //6
复制代码
若是您在面试中遇到了更多的原生JS问题,或者有一些本文未涉及到且有必定难度的JS知识,请给我留言。您的问题将会出如今后续文章中~
本文的写成耗费了很是多的时间,在这个过程当中,我也学习到了不少知识,谢谢各位小伙伴愿意花费宝贵的时间阅读本文,若是本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的确定是我前进的最大动力。github.com/YvetteLau/B…
后续写做计划
1.《寒冬求职季之你必需要懂的原生JS》(中)(下)
2.《寒冬求职季之你必需要知道的CSS》
3.《寒冬求职季之你必需要懂的前端安全》
4.《寒冬求职季之你必需要懂的一些浏览器知识》
5.《寒冬求职季之你必需要知道的性能优化》
针对React技术栈:
1.《寒冬求职季之你必需要懂的React》系列
2.《寒冬求职季之你必需要懂的ReactNative》系列
参考文章:
0.1 + 0.2 !== 0.3
此题答案大量使用了此篇文章的图文: juejin.im/post/5b90e0…