本文参考文章《一名【合格】前端工程师的自检清单》, 并对其中的部分题目进行了解答,若有遗漏或错误之处望大家指出纠正,共同进步。(点击题目展开答案!)
此文章 Markdown 源文件地址:github.com/zxpsuper/bl…
前端工程师吃饭的家伙,深度、广度一样都不能差。
1. JavaScript 规定了几种语言类型?
JavaScript中的每一个值都有它自己的类型,JavaScript规定了七种语言类型,他们是:
Undefined Null Boolean String Number Symbol Object
2. JavaScript 对象的底层数据结构是什么?
对象数据被存储于堆中 (如对象、数组、函数等,它们是通过拷贝和new出来的)。
引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。
3. Symbol 类型在实际开发中的应用、可手动实现一个简单的 Symbol?
ES6
引入了一种新的原始数据类型 Symbol
,表示独一无二的值。
symbol
类型的 key
不能被 Object.keys
和 for..of
循环枚举。因此可当作私有变量使用。
let mySymbol = Symbol('key');
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
4. JavaScript 中的变量在内存中的具体存储形式
JavaScript
中的变量分为基本类型和引用类型:
基本类型: 保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问
引用类型: 保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript
不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用
5. 基本类型对应的内置对象,以及他们之间的装箱拆箱操作
String(), Number(), Boolean()
装箱:就是把基本类型转变为对应的对象。装箱分为隐式和显示
// 隐式装箱: 每当读取一个基本类型的值时,后台会创建一个该基本类型所对应的对象。
// 在这个基本类型上调用方法,其实是在这个基本类型对象上调用方法。
// 这个基本类型的对象是临时的,它只存在于方法调用那一行代码执行的瞬间,执行方法后立刻被销毁。
let num=123;
num.toFixed(2); // '123.00'//上方代码在后台的真正步骤为
var c = new Number(123);
c.toFixed(2);
c = null;
// 显式装箱: 通过内置对象 Boolean、Object、String 等可以对基本类型进行显示装箱。
var obj = new String('123');
拆箱: 拆箱与装箱相反,把对象转变为基本类型的值。
Number([1]); //1
// 转换演变:
[1].valueOf(); // [1];
[1].toString(); // '1';Number('1'); //1
6. 理解值类型和引用类型
JavaScript中的变量分为基本类型和引用类型:
基本类型: 保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问
引用类型: 保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript
不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用
7. null 和 undefined 的区别
Number
转换的值不同,Number(null)
输出为 0
, Number(undefined)
输出为 NaN
null
表示一个值被定义了,但是这个值是空值
undefined
表示缺少值,即此处应该有值,但是还没有定义
8. 至少可以说出三种判断 JavaScript 数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
typeof
—— 返回给定变量的数据类型,可能返回如下字符串: 'undefined'——Undefined
'boolean'——Boolean
'string'——String
'number'——Number
'symbol'——Symbol
'object'——Object / Null (Null 为空对象的引用)
'function'——Function
// 对于一些如 error() date() array()无法判断,都是显示object类型
instanceof
检测 constructor.prototype
是否存在于参数 object
的原型链上,是则返回 true
,不是则返回 false
。 alert([1,2,3] instanceof Array) // true
alert(new Date() instanceof Date) // true
alert(function(){this.name="22";} instanceof Function) //true
alert(function(){this.name="22";} instanceof function) //false
// instanceof 只能用来判断两个对象是否属于实例关系,而不能判断一个对象实例具体属于哪种类型。
复制代码
constructor
—— 返回对象对应的构造函数。 alert({}.constructor === Object); => true
alert([].constructor === Array); => true
alert('abcde'.constructor === String); => true
alert((1).constructor === Number); => true
alert(true.constructor === Boolean); => true
alert(false.constructor === Boolean); => true
alert(function s(){}.constructor === Function); => true
alert(new Date().constructor === Date); => true
alert(new Array().constructor === Array); => true
alert(new Error().constructor === Error); => true
alert(document.constructor === HTMLDocument); => true
alert(window.constructor === Window); => true
alert(Symbol().constructor); => undefined
// null 和 undefined 是无效的对象,没有 constructor,因此无法通过这种方式来判断。
复制代码
Object.prototype.toString()
默认返回当前对象的 [[Class]]
。这是一个内部属性,其格式为 [object Xxx]
,是一个字符串,其中 Xxx
就是对象的类型。 Object.prototype.toString.call(new Date);//[object Date]
Object.prototype.toString.call(new String);//[object String]
Object.prototype.toString.call(Math);//[object Math]
Object.prototype.toString.call(undefined);//[object Undefined]
Object.prototype.toString.call(null);//[object Null]
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(123) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
// 比较全面
9. 可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用
隐式转换一般说的是 Boolean
的转换
在 if
语句中,null
,""
,undefinded
, 0
, false
都会被转化为 false
一般应用于对接口数据判空时使用
10. 出现小数精度丢失的原因,JavaScript 可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法
JavaScript
使用了 IEEE 754
规范,二进制储存十进制的小数时不能完整的表示小数
Number.MAX_VALUE
等于 1.7976931348623157e+308
,最大安全数字 Number.MAX_SAFE_INTEGER
等于 9007199254740991
100
或 1000
,变成整数再运算BigInt
大整数,它可以表示任意大小的整数,注意只能表示整数,而不受安全整数的限制1. 理解原型设计模式以及 JavaScript 中的原型规则
A. 所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性;
B. 所有的引用类型(数组、对象、函数),都有一个`__proto__`属性(隐式原型),属性值是一个普通的对象;
C. 所有的函数,都具有一个 `prototype`(显式原型),属性值也是一个普通对象;
D. 所有的引用类型(数组、对象、函数),其隐式原型指向其构造函数的显式原型;`(obj._proto_ === Object.prototype)`;
E. 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的 `__proto__` (即它的构造函数的 `prototype`)中去寻找;
2. instanceof 的底层实现原理,手动实现一个 instanceof
简单说就是判断实例对象的__proto__
是不是强等于对象的prototype
属性,如果不是继续往原型链上找,直到 __proto__
为 null
为止。
function instanceOf(obj, object) {//obj 表示实例对象,object 表示对象
var O = object.prototype;
obj = obj.__proto__;
while (true) {
if (obj === null)
return false;
if (O === obj) // 这里重点:当 O 严格等于 obj 时,返回 true
return true;
obj = obj.__proto__;
}
}
复制代码
3. 理解 JavaScript 的执行上下文栈,可以应用堆栈信息快速定位问题
执行上下文 就是当前 JavaScript
代码被解析和执行时所在环境的抽象概念, JavaScript
中运行任何的代码都是在执行上下文中运行。
执行上下文总共有三种类型:全局执行上下文, 函数执行上下文, Eval
函数执行上下文
执行栈,在其他编程语言中也被叫做调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。
4. 实现继承的几种方式以及他们的优缺点
详情请点击:《继承的几种实现方式》
5. 可以描述 new 一个对象的详细过程,手动实现一个 new 操作符
new
一个对象的详细过程:function Test() {}
const test = new Test();
复制代码
const obj = {}
constructor
属性为构造函数的名称,设置新对象的__proto__
属性指向构造函数的 prototype
对象
obj.constructor = Test;
obj.__proto__ = Test.prototype;
Test.call(obj)
new
操作符function myNew(Obj,...args){
var obj = Object.create(Obj.prototype);//使用指定的原型对象及其属性去创建一个新的对象
Obj.apply(obj,args); // 绑定 this 到obj, 设置 obj 的属性
return obj; // 返回实例
}
复制代码
6. 理解 es6 class 构造以及继承的底层实现原理
ES6
类的底层还是通过构造函数去创建的。// es6 Parent类实现
class Parent {
constructor(name,age){
this.name = name;
this.age = age;
}
speakSomething(){
console.log("I can speek chinese");
}
}
// 转化为
var Parent = function () {
function Parent(name, age) {
_classCallCheck(this, Parent); // 判断实例 Parent instanceof Parent(函数)是否为true
this.name = name;
this.age = age;
}
// 此方法通过使用 Object.defineProperty 为 function Parent 的 prototype 添加属性值
_createClass(Parent, [{
key: "speakSomething",
value: function speakSomething() {
console.log("I can speek chinese");
}
}]);
return Parent;
}();
复制代码
ES6
的继承实现//定义子类,继承父类
class Child extends Parent {
static width = 18
constructor(name,age){
super(name,age);
}
coding(){
console.log("I can code JS");
}
}
// 转化为
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(name, age) {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name, age));
}
_createClass(Child, [{
key: "coding",
value: function coding() {
console.log("I can code JS");
}
}]);
return Child;
}(Parent);
复制代码
这里其实就是多了一个 _inherits(Child, _Parent);
方法,实现了以下功能,具体可看文章《ES6类以及继承的实现原理》
//实现的结果是:
subClass.prototype.__proto__ = superClass.prototype
subClass.__proto__ = superClass // 实现静态属性的继承
1. 理解词法作用域和动态作用域
词法作用域也称静态作用域,javascript
采用静态作用域
静态作用域 —— 函数的作用域基于函数创建的位置。
动态作用域 —— 函数的作用域基于函数的使用位置。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 输出 1 。JavaScript 采用的是词法作用域,也称为静态作用域。相同的,动态作用域此代码应该输出 2
复制代码
2. 理解 JavaScript 的作用域和作用域链
作用域(scope)就是变量访问规则的有效范围。
在 JavaScript
中全局变量的作用域是全局的,在代码的任何地方都是有定义的。然而函数的参数和局部变量只在函数体内有定义。另外局部变量的优先级要高于同名的全局变量,也就是说当局部变量与全局变量重名时,局部变量会覆盖全局变量。
3. this的原理以及几种不同使用场景的取值
this的几种不同使用场景的取值 + JavaScript 的 this 原理
4. 闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
原理:闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
作用:闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
应用:1. 匿名自执行函数 2. 结果缓存 3. 封装局部变量
参考链接:《学习Javascript闭包(Closure)》
5. 理解堆栈溢出和内存泄漏的原理,如何防止
堆栈溢出 的产生是由于过多的函数调用,导致调用堆栈无法容纳这些调用的返回地址,一般在递归中产生。堆栈溢出很可能由无限递归(Infinite recursion)产生,但也可能仅仅是过多的堆栈层级.
参考链接:《内存泄漏与避免》
6. 如何处理循环的异步操作
1. 为何 try 里面放 return,finally 还会执行,理解其内部机制
在 try
语句中,在执行 return
语句时,要返回的结果已经准备好了,就在此时,程序转到 finally
执行了。
在转去之前,try
中先把要返回的结果存放到局部变量中去,执行完 finally
之后,在从中取出返回结果。
因此,即使finally
中对返回的结果进行了改变,但是不会影响返回结果。
它应该使用栈保存返回值。
2. JavaScript 如何实现异步编程,可以详细描述 EventLoop 机制
JavaScript
如何实现异步编程:
callback
(回调函数) 回调函数代表着,当某个任务处理完,然后需要做的事。比如读取文件,连接数据库,等文件准备好,或数据库连接成功执行编写的回调函数,又比如像一些动画处理,当动画走完,然后执行回调。
Promise
Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果,相比回调函数,Promise
提供统一的 API
,各种异步操作都可以用同样的方法进行处理。
Generator
(生成器)函数 Generator
函数是 ES6
提供的一种异步编程解决方案,其行为类似于状态机。
async/await
async/await
本质上还是基于 Generator
函数,可以说是 Generator
函数的语法糖,async
就相当于之前写的run函数(执行Generator
函数的函数),而 await
就相当于 yield
,只不过 await
表达式后面只能跟着 Promise
对象,如果不是 Promise
对象的话,会通过 Promise.resolve
方法使之变成 Promise
对象。async
修饰 function
,其返回一个 Promise
对象。
3. 宏任务和微任务分别有哪些
宏任务: setTimeout,setInterval,setImmediate (Node独有),requestAnimationFrame (浏览器独有),I/O,UI rendering (浏览器独有)
微任务: process.nextTick (Node独有),Promise,Object.observe,MutationObserver
4. 可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法
// 执行顺序,先微队列,后宏队列。
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(8);
})
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
setTimeout(() => {
console.log(10);
})
resolve()
}).then(() => {
console.log(5);
Promise.resolve().then(() => {
console.log(11)
});
setTimeout(() => {
console.log(13);
})
})
setTimeout(() => {
Promise.resolve().then(() => {
console.log(9)
});
console.log(6);
setTimeout(() => {
console.log(12);
})
})
console.log(7);
复制代码
从头至尾执行一次代码,根据上面分类规则分至不同队列, new promise( function )
也是立即执行。setTimeout
的回调函数属于宏队列(macrotask)
,resolve
的回调函数属于微队列
// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
// 宏队列
() => {
console.log(2);
setTimeout(() => {
console.log(8);
})
Promise.resolve().then(() => {
console.log(3)
});
}
() => {
console.log(10);
}
() => {
Promise.resolve().then(() => {
console.log(9)
});
console.log(6);
setTimeout(() => {
console.log(12);
})
}
复制代码
// 微队列
() => {
console.log(5);
Promise.resolve().then(() => {
console.log(11)
});
setTimeout(() => {
console.log(13);
})
}
复制代码
优先执行微队列,微队列执行过程中产生的微队列和宏队列置于队列末尾排序执行,而宏队列产生的微队列和宏队列于新的队列中等待。。
执行微队列:(分类)
// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
//////////
console.log(5);
// 微队列
() => {
console.log(11)
});
// 宏队列
() => {
console.log(2);
setTimeout(() => {
console.log(8);
})
Promise.resolve().then(() => {
console.log(3)
});
}
() => {
console.log(10);
}
() => {
Promise.resolve().then(() => {
console.log(9)
});
console.log(6);
setTimeout(() => {
console.log(12);
})
}
() => {
console.log(13);
}
复制代码
此时新增了一个微队列console.log(11)
,因为是微队列产生的,继续执行:
// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
//////////
console.log(5);
/////////
console.log(11)
// 微队列-空
// 宏队列
() => {
console.log(2);
setTimeout(() => {
console.log(8);
})
Promise.resolve().then(() => {
console.log(3)
});
}
() => {
console.log(10);
}
() => {
Promise.resolve().then(() => {
console.log(9)
});
console.log(6);
setTimeout(() => {
console.log(12);
})
}
() => {
console.log(13);
}
复制代码
执行完微队列后执行宏队列:
// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
//////////
console.log(5);
/////////
console.log(11);
/////////
console.log(2);
console.log(10);
console.log(6);
console.log(13);
// 微队列
() => {
console.log(3)
}
() => {
console.log(9)
}
// 宏队列
() => {
console.log(8);
}
() => {
console.log(12);
}
接下来执行微队列后宏队列,即:
// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
//////////
console.log(5);
/////////
console.log(11);
/////////
console.log(2);
console.log(10);
console.log(6);
console.log(13);
////////
console.log(3)
console.log(9)
////////
console.log(8);
console.log(12);
5. 使用 Promise 实现串行
// 一个 promise 的 function
function delay(time) {
return new Promise((resolve, reject) => {
console.log(`wait ${time}s`)
setTimeout(() => {
console.log('execute');
resolve()
}, time * 1000)
})
}
const arr = [3, 4, 5];
复制代码
reduce
arr.reduce((s, v) => {
return s.then(() => delay(v))
}, Promise.resolve())
复制代码
async
+ 循环 + await
(
async function () {
for (const v of arr) {
await delay(v)
}
}
)()
复制代码
let p = Promise.resolve()
for (const i of arr) {
p = p.then(() => delay(i))
}
复制代码
function dispatch(i, p = Promise.resolve()) {
if (!arr[i]) return Promise.resolve()
return p.then(() => dispatch(i + 1, delay(arr[i])))
}
dispatch(0)
复制代码
6. Node 与浏览器 EventLoop 的差异
《JavaScript 运行机制详解:再谈Event Loop》
7. 如何解决页面加载海量数据而页面不卡顿
window.requestAnimationFrame
和 document.createDocumentFragment()
实现, 可参考文章【如何解决页面加载海量数据而不冻结前端UI】DOM
结构最简单化。可参考文章【大数据如何在前端流畅展示】,不过他的 Demo
有点问题.1. 理解 ECMAScript 和 JavaScript 的关系
ECMAScript
是 JavaScript
的规范,JavaScript
是 ECMAScript
的实现。
2. 熟练运用 es5、es6 提供的语法规范
3. setInterval 需要注意的点,使用 settimeout 实现 setInterval
在使用 setInterval
方法时,每一次启动都需要对 setInterval
方法返回的值做一个判断,判断是否是空值,若不是空值,则要停止定时器并将值设为空,再重新启动,如果不进行判断并赋值,有可能会造成计时器循环调用,在同等的时间内同时执行调用的代码,并会随着代码的运行时间增加而增加,导致功能无法实现,甚至占用过多资源而卡死奔溃。因此在每一次使用setInterval方法时,都需要进行一次判断。
let timer = setInterval(func, 1000)
// 在其他地方再次用到setInterval(func, 1000)
if (timer !== null) {
clearInterval(timer)
timer = null
}
timer = setInterval(func, 1000)
setIntervalFunc = () =>{
console.log(1) //使用递归
setTimeout(setIntervalFunc, 1000);
};
setInterval()
复制代码
4. JavaScript 提供的正则表达式 API、可以使用正则表达式(邮箱校验、URL解析、去重等)解决常见问题
邮箱校验:
function isEmail(emailStr) {
return /^[a-zA-Z0-9]+([._-]*[a-zA-Z0-9]*)*@[a-zA-Z0-9]+.[a-zA-Z0-9{2,5}$]/.test(emailStr);
}
复制代码
URL解析:
function isUrl(urlStr) {
return /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\*\+,;=.%]+$/.test(value)
}
复制代码
数组去重:
// set结构
let arr = [1, 1, 2, 2, 3, 3]
arr2 = [...new Set(arr)]
console.log(arr2) // [1,2,3]
// Object.keys(), 利用属性 key 的唯一性
let arrObj = [1, 1, 2, 2, 3, 3]
arrObj2 = {}
for (i in arrObj) {
arrObj2[arrObj[i]] = true
}
let arrObj3 = Object.keys(arrObj2)
console.log(arrObj3)
// 利用 indexOf() 查询数组内是否已经包含该元素
var arrIndexOf = ['a','c','b','d','a','b']
var arrIndexOf2 = [];
for(var i = 0;iif(arrIndexOf2.indexOf(arrIndexOf[i])<0){
arrIndexOf2.push(arrIndexOf[i]);
}
}
console.log(arrIndexOf2)// ['a', 'c', 'b', 'd']
1. 从规范的角度理解 HTML,从分类和语义的角度使用标签
语义化标签:
等