知识体系来源于一名【合格】前端工程师的自检清单
winter
在他的《重学前端》课程中提到:
到现在为止,前端工程师已经成为研发体系中的重要岗位之一。可是,与此相对的是,我发现极少或者几乎没有大学的计算机专业愿意开设前端课程,更没有系统性的教学方案出现。大部分前端工程师的知识,其实都是来自于实践和工作中零散的学习。
一.JavaScript基础
变量和类型
-
Javascript规定了几种语言类型
七种内置类型null,undefined,string,number,boolean,object,symbol
undefined:1.undefined类型表示为定义,它的值只有一个undefined
2.任何变量赋值前都是undefined类型,值为undefined
3.undefined是一个变量不是关键字
null:1.只有一个值就是null 2.表示空值是关键字,可用null关键字获取null
string:1.string的意义并非字符串,而是字符串UTF16编码
2.字符串是永远无法变更的
number:1.number类型有264- 253+3 个值。
2.根据双精度浮点数定义,有效的整数范围是 -0x1fffffffffffff 至 0x1fffffffffffff,无法精确表示此范围外的整数。
3.根据双精度浮点数定义,非整数的Number类型无法用==来比较(三个等号也不行),正确的比较方法是用 JavaScript提供的最小精度值:
console.log( 0.1 + 0.2 == 0.3);//false
//正确的比较方法
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);//true
symbol:1.表示独一无二的值,它是一切非字符串的对象key的集合
2.Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型
object:object是js中最复杂的类型。也是js的核心机制之一,object是对象,在js中,对象的定义是“属性的集合”,属性分为数据属性和访问器属性,二者都是key-value的结构,key可以是字符串或者symbol类型
除了七种语言类型,还有规范类型 :
1.list和record:用于描述函数传参的过程
2.set:主要用于解释字符集等
3.completion record:用于描述异常、跳出等语句执行过程
4.reference:用于描述对象属性访问、delete等
5.property descriptor:用于描述对象的属性
6.lexical environment和environment record:用于描述变量和作用域
7.data block:用于描述二进制数据
-
Javascript对象的底层数据结构是什么
什么是数据结构?百度百科:数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。精心选择的数据结构可带来更高运行或者存储效率。
其实就是数据的事,结构组织。不再关注数据,而是关注组织数据,数据就事一堆书本,怎么摆放合适
常用的数据结构:1.栈和队列 2.单向链接列表和双重链接列表 3.树(深度优先搜索和广度优先搜索)
堆是动态分配内存,内存大小不一,也不会自动释放
栈是自动分配相对固定大小的内存空间,并由系统自动释放,栈现金后出,队列后劲先出
JS基本类型数据都是直按值存储在栈中的(undefined、null、不是new出来的布尔,数字,字符串),每种类型的数据占用的内存空间大小是确定的,更容易管理内存空间
JS引用类型数据存储于堆中(如对象,数据,函数等),引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据
数据在内存中的存储结构分为两种:
顺序存储结构:吧数据元素存放在地址连续的存储单元里比如数组
链式存储结构:把数据元素存放在内次年任意的存储单元内比如链表
JavaScript的数据结构,ES5中自带的array、object,ES6自带的set、map、weakset、weakmap
在JavaScript中不管多复杂的数据,都可以组织成object形式的对象。对象大多表现为Dictionary如{a: foo,b :bar}
-
Symbol
类型在实际开发中的应用、可手动实现一个简单的Symbol
由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
应用1:使用Symbol来作为对象属性名(key)
Symbol类型的key是不能通过Object.keys()
或者for...in
来枚举的,因此我们可把不需对外操作和访问属性使用symbol定义,如想获取可以使用Object.getOwnPropertySymbols()或Reflect.ownKeys()
应用2:使用Symbol来代替常量
应用3:使用Symbol定义类的私有属性/方法
应用4:注册和获取全局Symbol
使用Symbol.for()可以注册或获取一个window间全局的Symbol实例
-
JavaScript
中的变量在内存中的具体存储形式
JavaScript中的变量分为基本类型和引用类型
基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问
引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用
-
基本类型对应的内置对象,以及他们之间的装箱拆箱操作
String()、Number()、Boolean()、RegExp()、Date()、Error()、Array()、
Function()、Object()、symbol();类似于对象的构造函数
装箱转换:基本类型 -> 对象
1.每一种基本类型,都在对象中有对应的类,装箱机制会频繁产生临时对象
2.使用object函数可以显示调用装箱能力
3.每一类装箱对象皆有私有的 Class 属性,这些属性可以Object.prototype.toString 获取。在 JavaScript 中,没有任何方法可以更改私有的Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。
4.call函数本身会产生装箱操作,需要配合typeof来区分基本类型还是对象类型。
拆箱转换:对象 -> 基本类型
1.ToPrimitive 函数,它是对象类型到基本类型的转换
2.拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果valueOf 和 toString都不存在,或者没有返回基本类型,则会产生TypeError。
-
理解值类型和引用类型
值类型:string、number、boolean、undefined、null
1.占用空间固定,保存在栈中
2.保存与复制的是值的本身
3.使用typeof检测数据类型
4.基本类型数据是值类型
引用类型:Object、Array、Function
1.占用空间不固定,保存在堆内
2.保存于复制的是只想对象的一个指针
3.使用instanceof检测数据类型
4.使用new()方法构造出的对象是引用型的
-
null
和undefined
的区别
Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。
Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。
-
至少可以说出三种判断
JavaScript
数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
1.typeof():无法判断null与object,无法区分引用数据类型
2.instanceof:判断一个变量是否是某个对象的实例,无法对原始类型进行判断
3.Object.prototype.toString.call():此方法提供了一个通用的数据类型判断模式,但不能判断自定义类
4.constructor:constructor属性返回对创建此对象的数组函数的引用,返回对象相对应的构造函数
在MDN中就比较了Array.isArray和instanceof的区别,当Array.isArray()不可用的使用,MDN做了如下的补丁,说明还是比较推荐使用前面讲的第三种方法 Object.prototype.toString.call(obj)。
-
可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用
在JS中数据类型隐式转换分三种情况:
1.转换为布尔类型 :
数据类型 | 转换后的值 |
0 | false |
NaN | false |
空字符 | false |
null | false |
undefined | false |
非0数字 | true |
非空字符串 | true |
非null对象类型 | true |
连续使用两个非操作符(!!)可以将一个数强制转换为boolean类型
2.转换为number类型
3.转换为string类型
操作符会影响数据的类型转换
Typescript避免
-
出现小数精度丢失的原因,
JavaScript
可以存储的最大数字、最大安全数字,JavaScript
处理大数字的方法、避免精度丢失的方法。
由于进制问题导致的小数相乘精度不准确,eg:
let a = 0.00001;
let b = 0.00002;
let c = 0.00003;
a + b === c //false
最大数字2的53次方减一
解决办法
把小数放到位整数(乘倍数),再缩小回原来倍数(除倍数)
function accMul(arg1,arg2){
var m=0,s1=arg1.toString(),s2=arg2.toString();
try{m+=s1.split(".")[1].length}catch(e){}
try{m+=s2.split(".")[1].length}catch(e){}
return (Number(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,m)).toFixed(2);
}
原型和原型链
-
理解原型设计模式以及JS中的原型规则
何为原型?所有对象有私有字段[prototype],就是对象的原型。读取一个对象的属性,如果对象本身没有就到对象的原型上找,直到原型为空
设计模式
1.工厂模式:在函数内创建一个对象,给对象赋予属性及方法再将对象返回
function Person() {
var People = new Object();
People.name = 'CrazyLee';
People.age = '25';
People.sex = function(){
return 'boy';
};
return People;
}
var a = Person();
console.log(a.name);//CrazyLee
console.log(a.sex());//boy
2.构造函数模式:无需在函数内部重新创建对象,而是用this指代
function Person() {
this.name = 'CrazyLee';
this.age = '25';
this.sex = function(){
return 'boy'
};
}
var a = new Person();
console.log(a.name);//CrazyLee
console.log(a.sex());//boy
3.原型模式:函数中部队属性进行定义,利用prototype属性对属性进行定义,可以让所有对象实例共享它所包含的属性及方法
function Parent() {
Parent.prototype.name = 'carzy';
Parent.prototype.age = '24';
Parent.prototype.sex = function() {
var s="女";
console.log(s);
}
}
var x =new Parent();
console.log(x.name); //crazy
console.log(x.sex()); //女
4.混合模式:原型模式 + 构造函数模式。构造函数模式用于定义实例属性,原型模式用于定义方法和共享属性
function Parent(){
this.name="CrazyLee";
this.age=24;
};
Parent.prototype.sayname=function(){
return this.name;
};
var x =new Parent();
console.log(x.sayname());
5.动态原型模式:将所有信息封装在构造函数中,通过构造函数中初始化原型,可以通过判断该方法是否有效而选择是否需要初始化原型
function Parent(){
this.name="CrazyLee";
this.age=24;
if(typeof Parent._sayname=="undefined"){
Parent.prototype.sayname=function(){
return this.name;
}
Parent._sayname=true;
}
};
var x =new Parent();
console.log(x.sayname())
原型规则
1.所有引用类型,都具有对象特征,可自由扩展属性
2.所有的引用类型,都有一个_proto_属性(隐式原型),属性只是一个普通对象
3.所有函数都具有一个prototype(显式原型),属性值也是一个普通原型
4.所有的因通用类型,其隐式原型只想其构造函数的显式原型(obj.proto === Object.prototype)
5.当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去他的_proto_中去寻找
原型对象:prototype 在js中,函数对象其中一个属性:原型对象prototype。普通对象没有prototype属性,但有_proto_属性。 原型的作用就是给这个类的每一个对象都添加一个统一的方法,在原型中定义的方法和属性都是被所以实例对象所共享。
var person = function(name){
this.name = name
};
person.prototype.getName=function(){//通过person.prototype设置函数对象属性
return this.name;
}
var crazy= new person(‘crazyLee’);
crazy.getName(); //crazyLee//crazy继承上属性
原型链:当试图得到一个对象f的某个属性时,如果这个对象本身没有这个属性,那么会去它的_proto_(即它的构造函数的prototype)obj._proto_中去寻找;当obj._proto也没有时,便会在obj._proto.proto(即obj的构造函数的prototype的构造函数的prototype)中寻找;
-
instanceof的底层实现原理,手动实现一个instanceof
instanceof 的作用:用于判断一个引用类型是否属于某构造函数,还可以在继承关系中用来判断一个实例是否属于它的父类型
instance底层工作原理
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__;
}
}
手动实现instanceof
1.直接使用instanceof工作原理?
2.在方法一的基础上使用constructor
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R;
L = L.__proto__;
while (true) {
if (L === null)
return false;
if (O === L.constructor) // 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}
-
实现继承的几种方式以及他们的优缺点
在ES6中有了继承,使用extends关键字就能实现,在ES6之前的继承方式
1.原型链
基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
原型链虽然很强大,但存在两个问题:(1)包含引用类型值的原型属性会被所有实例共享,这回到这对一个实例的修改会影响另一个实例 (2)在创建子类型的实例时,不能向超类型的构造函数中传递参数
function SuperType(){
this.prototype=true;
}
SuperType.prototype.getSuperValue=function(){
return this.property;
}
function SubType(){
this.subproperty=false;
}
//通过创建SuperType的实例继承了SuperType
SubType.prototype=new SuperType();
SubType.prototype.getSubValue=function(){
return this.subproperty;
}
var instance=new SubType();
alert(instance.getSuperValue()); //true
function SuperType(){
this.colors=["red", "blue", "green"];
}
function SubType(){
}
//继承了SuperType
SubType.prototype=new SuperType();
var instance1=new SubType();
instance1.colors.push("black");
alert(instance1.colors); //red,blue,green,black
var instance2=new SubType();
alert(instance2.colors); //red,blue,green,black
2.借用构造函数
在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此可通过使用apply()和call()方法在新创建的对象上执行构造函数
function SuperType(){
this.colors=["red", "blue", "green"];
}
function SubType(){
//继承SuperType
SuperType.call(this);
}
var instance1=new SubType();
instance1.colors.push("black");
alert(instance1.colors); //red,bllue,green,black
var instance2=new SubType();
alert(instance2.colors); //red,blue,green
相比于原型链,借用构造函数可在子类型构造函数中向超类型构造函数传递参数
function SuperType(name){
this.name=name;
}
function SubType(){
//继承了SuperType,同时还传递了参数
SuperType.call(this,"mary");
//实例属性
this.age=22;
}
var instance=new SubType();
alert(instance.name); //mary
alert(instance.age);
存在两个问题:(1)无法避免构造函数模式存在的问题,方法都在构造函数中定义,因此无法复用函数
(2)在超类型的原型中定义的方法,对子类型而言是不可见的
3.组合继承
将原型链和借用构造函数组合一起,使用原型链实现对原型方法的继承,通过借用构造函数来实现对实例属性的继承,这样,既通过在原型上定义方法实现了函数的复用,又能够保证每个实例都有它自己的属性
function SuperType(name){
this.name=name;
this.colors=["red", "blue", "green"];
}
SuperType.prototype.sayName=function(){
alert(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(){
alert(this.age);
};
var instance1=new SubType("mary", 22);
instance1.colors.push("black");
alert(instance1.colors); //red,blue,green,black
instance1.sayName(); //mary
instance1.sayAge(); //22
var instance2=new SubType("greg", 25);
alert(instance2.colors); //red,blue,green
instance2.sayName(); //greg
instance2.sayAge(); //25
-
至少说出一种开源项目(如Node)中应用原型继承的案例
node中util.inherits()
export.inherits = function (ctor, superCtor){
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
}
util.inherit只是继承了父类原型链里的方法,还有super_
只是构造函数的一个属性。而second.prototype = new first();
继承了所有方法。也就是说util.inherits
相当于second.prototype = first.prototype;
-
可以描述new一个对象的详细过程,手动实现一个new操作符
new操作发生了什么?:
1.以构造器的prototype属性为原型,创建对象
2.将this和调用参数传给构造器,执行
3.如果构造器返回的是对象,则放回,否则返回第一步创建的对象
详细过程: 1.创建一个空对象
var obj = new Object();
2.让Person中的this指向obj,并执行Person的函数体
var result = Person.call(obj);
3.设置原型链,将obj的_proto_成员只想Person函数对象的prototype成员对象
obj.__proto__ = Person.prototype;
4.判断person的返回值类型,如果是值类型返回obj。如果是引用类型返回这个引用类型的对象
if (typeof(result) == "object")
person = result;
else
person = obj;
手动实现:创建空对象,链接到原型,绑定this值,返回新对象
function create(){
//创建一个空对象
let obj = new Object();
//获取构造函数
let Constructor = [].shift.call(arguments);
//链接到原型
obj.__proto__ = Constructor.prototype;
//绑定this值
let result = Constructor.apply(obj,arguments);//使用apply,将构造函数中的this指向新对象,这样新对象就可以访问构造函数中的属性和方法
//返回新对象
return typeof result === "object" ? result : obj;//如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
}
-
理解es6 class构造以及继承的底层实现原理
ES6中的类,可以看作构造函数的另一个写法,类的数据类型就是函数,类本身就指向构造函数
function People(name,age){
this.name = name;
this.age = age;
}
People.prototype.say=function(){
console.log("hello)}
People.see=function(){
alert("how are you")}
class People{
constructor(name,age){
this.name = name;
this.age = age}
static see(){alert("how are you")} }
say(){console.log("hello");}}
Class可以通过extends关键字实现继承,实质是先将父类实例对象的属性和方法加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this
作用域和闭包
-
理解词法作用域和动态作用域
作用域是指程序源代码中定义变量的区域,规定了如何查找代码。就是变量与函数的可访问范围
词法作用域,也叫静态作用域,它的作用域是指再词法分析阶段就确定了,不会改变。
动态作用域是在运行时根据程序的流程信息来动态确定的,而不是写代码是进行静态确定的
-
理解Javascript的作用域和作用域链
JS中作用域控制着变量与函数的可见性和生命周期。变量的作用域分为
1.全局作用域:在代码任何地方都能访问到的对象拥有全局作用域
(1)在最外层函数和在最外层函数外面定义的变量拥有全局作用域
(2)所有未定义直接赋值的变量自动声明为拥有全局作用域
(3)所有window对象的属性拥有全局作用域
2.局部作用域:函数内部可访问到
3.块级作用域:let为JS新增了块级作用域
作用域链:函数内部属性[Scope]包含了函数被创建的作用域中的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。变量都是对象的属性,而该对象可能又是其它对象的属性,而所有的对象都是全局对象的属性,所以这些对象的关系可以看作是一条链,链头就是变量所处的对象,链尾就是全局对象。
-
理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
当JavaScript代码执行的时候回进入不同的执行环境(执行上下文),这些执行环境会构成一个执行环境栈(执行上下文栈)
执行上下文栈是执行上下文的活动记录(数据的出栈和压栈)。
1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2. 在全局执行上下文确定后, 将其添加到栈中(压栈)
3. 在函数执行上下文创建后, 将其添加到栈中(压栈)
4. 在当前函数执行完后,将栈顶的对象移除(出栈)
5. 当所有的代码执行完后, 栈中只剩下在全局执行上下文
Error.captureStackTrace 会捕获堆栈信息, 并在第一个参数中创建 stack 属性来存储捕获到的堆栈信息. 如果提供了第二个参数, 该函数将作为堆栈调用的终点. 因此, 捕获到的堆栈信息将只显示该函数调用之前的信息.
-
this的原理以及几种不同使用场景的取值
this指的是函数运行时所在的环境。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2
-
闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
闭包就是能够读取其他函数内部变量的函数。闭包可以简单理解成“定义在一个函数内部的函数“,在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
作用:1.可以读取函数内部的变量。 2.让这些变量的值始终保持在内存中。
实际应用:1.setTimeout
function func(param) {
return function() {
alert(param);
}
}
var f = func(1)
setTimeout(f, 1000); // 原生的setTimeout有一个缺陷,你传递的第一个函数不能带参数
2.封装变量。即把变量隐藏起来
function isFirstLoad() {
var _list = []
return function (id) {
if (_list.indexOf(id) >= 0) {
return false
} else {
_list.push(id)
return true
}
}
}
// 使用
var firstLoad = isFirstLoad()
firstLoad(10) // true
firstLoad(10) // false
firstLoad(20) // true
-
理解堆栈溢出和内存泄漏的原理,如何防止
程序代码运行需要计算空间,就是栈,就像小学生算术需要一张演算纸一样。//这里不考虑堆,或者堆就是多那几张草稿纸。
每个子函数都需要一些局部变量,这需要在演算纸上占用空间。
程序从栈底开始计算,随着各个子函数的调用(入栈)和返回(出栈),占用的栈资源也不断的增加减少。这个过程如果能可视化,就是那个音乐节奏灯,忽闪忽闪的一会高一会儿低的 节奏灯。
栈溢出的意思就是系统分配的演算纸不够用了,你把数字写到纸外面了。
递归调用容易产生这个,是因为递归的层级是动态的,不像非递归程序, 非递归程序中存在一个最深的函数调用链,只要最深的这个链不栈溢出就可以了,而一般递归无法给出这个保证,若递归层次太深就栈溢出了。尾递归可以解决这个问题。
尾调用尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录。
尾递归:函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
-
如何处理循环的异步操作
回调函数,事件监听,发布/订阅,ES6中的Promise和Generator。
-
理解模块化解决的实际问题,列举几个模块化方案并理解其中原理
前端模块化主要解决两个问题:命名空间冲突,文件依赖管理
模块化构建工具:webpack/requireJS/seaJS等是用来组织前端模块的构建工具
模块化规范:AMD/CMD/CommonJS/es6模块等等规范,规范了如何来组织你的代码
执行机制
-
为何
try
里面放return
,finally
还会执行,理解其内部机制
finally是在return后面的表达式运算之后执行的,此时并没有返回运算之后的值,而是把值保存起来,不管finally对该值做任何的改变,返回的值都不会改变,依然返回保存起来的值。也就是说方法的返回值是在finally运算之前就确定了的。
先执行try中的语句,包括return后面的表达式,
有异常时,先执行catch中的语句,包括return后面的表达式,
然后执行finally中的语句,如果finally里面有return语句,会提前退出,
最后执行try中的return,有异常时执行catch中的return。
-
JavaScript
如何实现异步编程,可以详细描述EventLoop
机制
JavaScript是单线程,实现异步操作的方式借助了浏览器的其他线程的帮助。其他线程通过任务队列(task queue)和事件循环(event loop)。
任务队列:在JavaScript异步机制中,任务队列就是用来维护异步任务回调函数的队列。这样一个队列用来存放这些回调函数,它们会等到主线程执行完所有的同步函数之后按照先进先出的方式挨个执行。
事件循环:主线程在执行完同步任务之后,会无限循环地去检查任务队列中是否有新的“任务”,如果有则执行。而这些任务包括我们在异步任务中定义的回调函数,也包括用户交互事件的回调函数。通过事件循环,Javascript不仅很好的处理了异步任务,也很好的完成了与用户交互事件的处理。因为在完成异步任务的回调函数之后,任务队列中的任务都是由事件所产生的,因此我们也把上述的循环过程叫做事件循环。
-
宏任务和微任务分别有哪些
宏任务:包括整体代码script,setTimeout,setInterval
微任务:Promise,process.nextTick(类似node.js版的"setTimeout")
-
可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
// 1 4 5 2 3
整体script作为第一个宏任务进入主线程,输出1
遇到setTimeout,回调函数分发到宏任务Event Queue中
遇到Promise,直接执行new Promise,输出4
then备份发到微任务Event Queue
第一轮宏任务事件循环结束,执行微任务输出5
第一轮事件循环结束,第二轮事件循环从setTimeout宏任务开始,输出2
遇到process.nextTick(),分发到微任务Event Queue
第轮二宏任务事件循环结束,执行微任务输出3
-
使用Promise实现串行
使用Promise.all()实现并行
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
Promise实现串行
function promiseQueue (executors) {
return new Promise((resolve, reject) => {
if (!Array.isArray(executors)) { executors = Array.from(executors) }
if (executors.length <= 0) { return resolve([]) }
var res = []
executors = executors.map((x, i) => () => {
var p = typeof x === 'function' ? new Promise(x) : Promise.resolve(x)
p.then(response => {
res[i] = response
if (i === executors.length - 1) {
resolve(res)
} else {
executors[i + 1]()
}
}, reject)
})
executors[0]()
})
}
-
Node与浏览器EventLoop的差异
node的事件轮询与浏览器不太一样
timers阶段:执行setTimeout,setInterval的callback回调
I/O callbacks阶段:执行除了close事件的callbacks、被timers(定时器,setTimeout、setInterval 等)设定的 callbacks 、setImmediate() 设定的 callbacks 之外的 callbacks
idle,prepare阶段:node 内部使用,Process.nextTick 在此阶段执行
poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
check 阶段:执行 setImmediate() 的回调函数
close callbacks 阶段:执行 close 事件的 callback ,例如 socket.on('close', callback);
setTimeout(function () {
console.log('execute in first timeout');
Promise.resolve(3).then(res => {
console.log('execute in third promise');
});
}, 0);
setTimeout(function () {
console.log('execute in second timeout');
Promise.resolve(4).then(res => {
console.log('execute in fourth promise');
});
}, 0);
Promise.resolve(1).then(res => {
console.log('execute in first promise');
});
Promise.resolve(2).then(res => {
console.log('execute in second promise');
});
基于node:
1.promise也就是micor task 在下个阶段之前被调用
2.当处于timers阶段时,会将对应的timer队列处理完,故 2个timeout会先被执行,再会进入下面一个状态,此时才会调用promise
execute in first promise
execute in second promise
execute in first timeout
execute in second timeout // node 先执行
execute in third promise
execute in fourth promise
-
如何在保证页面运行流畅的情况下处理海量数据
首选分页
策略:显示三屏数据,其他的移除 DOM。
语法和API
-
理解ECMAScript和JavaScript的关系
在阮一峰的《ES6入门》提到:ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。日常场合,这两个词是可以互换的。
-
熟练运用es5、es6提供的语言规范
-
熟练掌握JavaScript提供的全局对象(例如Data、Math)、全局函数、全局属性
全局对象包括:Array、Boolean、Number、String、RegExp、Data、Math、Function、Events
-
熟练应用map、reduce、filter等高阶函数解决问题
-
setInterval需要注意的点,使用settimeout实现setInterval
setInterval() 方法可以按照值定的周期来调用函数或计算表达式。setInterval() 方法会不停地调用函数,直到 clearInterval() 被调用或窗口被关闭。
注意点:1.当使用setInterval时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。
2.不能传递带参数的函数
3.
setInterval 周期性的调用函数或计算方法,关闭用clearInterval 。setInterval 和clearInterval 是一对一的关系。比如想要对同一个按钮在不同场景中,使用周期性的调用不同的函数,那么需要先关掉上一个setInterval,再设定另一个setInterval不然上一个setInterval仍然在进行着。
迭代setTimeout
为了避免setInterval()定时器的问题,可以使用链式setTimeout()调用
setTimeout(function fn(){
setTimeout(fn,interval);
},interval);
在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。
-
JavaScript提供的正则表达式API、可以使用正则表达式解决常见问题(邮箱校验、URL解析、去重等)
search()和replace()方法
使用RegExp对象有test()检测一个字符串是否匹配某个模式,使用exec()检索字符串中的正则表达式的匹配
/*是否带有小数*/
function isDecimal(strValue ) {
var objRegExp= /^\d+\.\d+$/;
return objRegExp.test(strValue);
}
/*校验是否中文名称组成 */
function ischina(str) {
var reg=/^[\u4E00-\u9FA5]{2,4}$/; /*定义验证表达式*/
return reg.test(str); /*进行验证*/
}
/*校验是否全由8位数字组成 */
function isStudentNo(str) {
var reg=/^[0-9]{8}$/; /*定义验证表达式*/
return reg.test(str); /*进行验证*/
}
/*校验电话码格式 */
function isTelCode(str) {
var reg= /^((0\d{2,3}-\d{7,8})|(1[3584]\d{9}))$/;
return reg.test(str);
}
/*校验邮件地址是否合法 */
function IsEmail(str) {
var reg=/^\w+@[a-zA-Z0-9]{2,10}(?:\.[a-z]{2,4}){1,3}$/;
return reg.test(str);
}
/*匹配URL*/
/^(https?:\/\/)([0-9a-z.]+)(:[0-9]+)?([/0-9a-z.]+)?(\?[0-9a-z&=]+)?(#[0-9-a-z]+)?/i
https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]
/*去重*/
var str = "abcabcccddf";
var res = str.replace(/(.).*\1/g,"$1")
-
JavaScript异常处理的方式,统一的异常处理方案
JS可以通过 try...catch..finally语句构造+throw运算符 来处理异常
try {
// 运行代码
[break;]
}
catch ( e ) {
// 如果发生异常,则运行代码
[break;]
}
[ finally {
// 无论如何,始终执行的代码
// 异常发生
}]