知识体系来源于一名【合格】前端工程师的自检清单

winter在他的《重学前端》课程中提到:

到现在为止,前端工程师已经成为研发体系中的重要岗位之一。可是,与此相对的是,我发现极少或者几乎没有大学的计算机专业愿意开设前端课程,更没有系统性的教学方案出现。大部分前端工程师的知识,其实都是来自于实践和工作中零散的学习。

一.JavaScript基础

变量和类型

  • Javascript规定了几种语言类型

JavaScript语言中类型集合又原始值和对象组成。

八种内置类型null、undefined、string、number、boolean、symbol、BigInt和对象Object

原始值

除对象类型(object)以外的其它任何类型定义的不可变的值(值本身无法被改变)。例如(与 C 语言不同),JavaScript 中字符串是不可变的(译注:如,JavaScript 中对字符串的操作一定返回了一个新字符串,原始字符串并没有被改变)。我们称这些类型的值为“原始值”。

undefined:
1.undefined类型表示为定义,它的值只有一个undefined
2.任何变量赋值前都是undefined类型,值为undefined
3.undefined是一个变量不是关键字,它引用了不存在的东西,并且该变量没有被定义为任何东西

null:
1.只有一个值就是null,就是一个被定义却缺少值的变量   
2.表示空值是关键字,可用null关键字获取null

string:
1.string的意义并非字符串,而是字符串UTF16编码            
2.字符串是永远无法变更的
3.JavaScript 的字符串类型用于表示文本数据。它是一组 16 位的无符号整数值的“元素”。在字符串中的每个元素占据了字符串的位置。第一个元素的索引为 0,下一个是索引 1,依此类推。字符串的长度是它的元素的数量

布尔类型:布尔表示一个逻辑实体,可以有两个值:true 和 false

number:
1.number类型有(从 -(2^53 -1) 到 2^53 - 1 之间的数字)。
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

BigInt:
是ES10新出的一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数,可以表示任意大的整数

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值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

Symbol() 函数会返回 symbol 类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的 symbol 注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

理解和使用ES6中的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实例

// 当调用 Symbol 的时候,会采用以下步骤:
//1. 如果使用 new ,就报错
//2. 如果 description 是 undefined,让 descString 为 undefined
//3. 否则 让 descString 为 ToString(description)
//4. 如果报错,就返回
//5. 返回一个新的唯一的 Symbol 值,它的内部属性 [[Description]] 值为 descString
(function () {var root = this;var generateName = (function () {var postfix = 0;return function (descString) {postfix++;return '@@' + descString + '_' + postfix}})()var SymbolPolyfill = function Symbol(description) {// 实现特性第 2 点:Symbol 函数前不能使用 new 命令if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');// 实现特性第 5 点:// 如果 Symbol 的参数是一个对象,toString 方法,将其转为字符串,然后才生成一个 Symbol 值。var descString = description === undefined ? undefined : String(descString);var symbol = Object.create({toString: function () {return this.__Name__;},valueOf: function () {return this;}})// 语法: Object.defineProperties(obj, props)// obj: 将要被添加属性或修改属性的对象// props: 该对象的一个或多个键值对定义了将要为对象添加或修改的属性的具体配置Object.defineProperties(symbol, {'__Description__': {value: descString,writable: false,enumerable: false,configurable: false},'__Name__': {value: generateName(descString),writable: false,enumerable: false,configurable: false}})// 实现特性第 6 点,因为调用该方法,返回的是一个新对象,两个对象之间,只要引用不同,就不会相同//  Symbol 函数的参数只是表示对当前 Symbol 值的描述,相同参数的 Symbol 函数的返回值是不相等的。return symbol}var forMap = {};// Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 keyObject.defineProperties(SymbolPolyfill, {'for': {value: function (description) {var descString = description === undefined ? undefined : String(description)return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);},writable: true,enumerable: false,configurable: true},'keyFor': {value: function (symbol) {for (var key in forMap) {if (forMap[key] === symbol) return key;}},writable: true,enumerable: false,configurable: true}})root.SymbolPolyfill = SymbolPolyfill
})();

  • 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检测数据类型,除了null

4.基本类型数据是值类型

引用类型:Object、Array、Function

1.占用空间不固定,保存在堆内

2.保存于复制的是只想对象的一个指针

3.使用instanceof检测数据类型

4.使用new()方法构造出的对象是引用型的


  • nullundefined的区别

Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。
Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。


  • 至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型

1.typeof():
无法判断null与object,无法区分引用数据类型
对于引用类型,除function以外,一律返object类型;

2.instanceof:判断一个变量是否是某个对象的实例,无法对原始类型进行判断

3.Object.prototype.toString.call():此方法提供了一个通用的数据类型判断模式,但不能判断自定义类

4.constructor:constructor属性返回对创建此对象的数组函数的引用,返回对象相对应的构造函数

在MDN中就比较了Array.isArray和instanceof的区别,当Array.isArray()不可用的使用,MDN做了如下的补丁,说明还是比较推荐使用前面讲的第三种方法 Object.prototype.toString.call(obj)。


  • 可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用

隐式类型转换的场景:

  • 使用if作判断的时候
  • 运算符的转换,例如在做+运算操作时,1+'2' // '12'
  • 对象的转换

转换原则: 基本上大多数类型做隐式转换时都会调用valueOftoString

在JS中数据类型隐式转换分三种情况:

1.转换为布尔类型 :

数据类型 转换后的值
0 false
NaN false
空字符 false
null false
undefined false
非0数字 true
非空字符串 true
非null对象类型 true

连续使用两个非操作符(!!)可以将一个数强制转换为boolean类型

2.转换为number类型

3.转换为string类型

应用及避免:

  • 在做if判断时可以直接使用undefined或者null与布尔值的转换、数字0和1与布尔值的转换以及空字符串与布尔值的转换
  • 把字符串类型转换为数字

操作符会影响数据的类型转换

Typescript避免


  • 出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法。

小数精度丢失的原因:Number类型采用IEEE754标准中的“双精度浮点数”来表示一个数字,不区分整数和浮点数

处理大数字的方法:可以采用字符串进行存储和处理

由于进制问题导致的小数相乘精度不准确,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隐式原型 时,返回truereturn 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 时,返回 truereturn 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());  //truefunction 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,blackvar instance2=new SubType();
alert(instance2.colors);    //red,blue,green,black

2.借用构造函数

在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此可通过使用apply()和call()方法在新创建的对象上执行构造函数

function SuperType(){this.colors=["red", "blue", "green"];
}
function SubType(){//继承SuperTypeSuperType.call(this);
}
var instance1=new SubType();
instance1.colors.push("black");
alert(instance1.colors);  //red,bllue,green,blackvar 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)实例并不是父类的实例,只是子类的实例
(4)只能继承父类的实例属性和方法,不能继承原型属性/方法

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();  //22var instance2=new SubType("greg", 25);
alert(instance2.colors);   //red,blue,green
instance2.sayName();  //greg
instance2.sayAge();  //25

4.实例继承

function Cat(name){ var instance = new Animal();instance.name = name || 'Tom'; return instance;
}

特点:

  • 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  • 实例是父类的实例,不是子类的实例
  • 不支持多继承

5.拷贝继承

function Cat(name){ var animal = new Animal(); for(var p in animal){ Cat.prototype[p] = animal[p]; } Cat.prototype.name = name || 'Tom';
}

特点:

  • 支持多继承

缺点:

  • 效率较低,内存占用高(因为要拷贝父类的属性)
  • 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in访问到)

6. 寄生继承

function Cat(name){ Animal.call(this); this.name = name || 'Tom';
}(function(){ // 创建一个没有实例方法的类var Super = function(){};Super.prototype = Animal.prototype;//将实例作为子类的原型Cat.prototype = new Super();
})();Cat.prototype.constructor = Cat;

特点:

  • 堪称完美

缺点:

  • 实现较为复杂

  • 至少说出一种开源项目(如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;
elseperson = 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;//如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
}or//自己定义的new方法
let newMethod = function (Parent, ...rest) {// 1.以构造器的prototype属性为原型,创建新对象;let child = Object.create(Parent.prototype);// 2.将this和调用参数传给构造器执行 //   取得构造函数的返回值const ret = Parent.apply(child, rest);// 3.返回第一步的对象//   如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象return ret instanceof Object ? ret : this;
};

  • 理解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

var Book = function () {function Book(props) {_classCallCheck(this, Book);this._title = props.title;}_createClass(Book, [{key: "toString",// 省略...}, {key: "title",// 省略...}], [{key: "staticMethod",// 省略...}]);return Book;
}();var EBook = function (_Book) {function EBook(props) {// 省略...}_inherits(EBook, _Book);_createClass(EBook, [{key: "toString",// 省略...}, {key: "link",// 省略...}]);return EBook;
}(Book);

作用域和闭包

  • 理解词法作用域和动态作用域

作用域是指程序源代码中定义变量的区域,规定了如何查找代码。就是变量与函数的可访问范围

词法作用域:也叫静态作用域,它的作用域是指再词法分析阶段就确定了,不会改变。

动态作用域:是在运行时根据程序的流程信息来动态确定的,而不是写代码是进行静态确定的

JavaScript采用词法作用域


  • 理解Javascript的作用域和作用域链

JS中作用域控制着变量与函数的可见性和生命周期。变量的作用域分为

1.全局作用域:
在代码任何地方都能访问到的对象拥有全局作用域
(1)在最外层函数和在最外层函数外面定义的变量拥有全局作用域
(2)所有未定义直接赋值的变量自动声明为拥有全局作用域
(3)所有window对象的属性拥有全局作用域

2.局部作用域:函数内部可访问到

3.块级作用域:let、const为JS新增了块级作用域

作用域链:函数内部属性[Scope]包含了函数被创建的作用域中的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。变量都是对象的属性,而该对象可能又是其它对象的属性,而所有的对象都是全局对象的属性,所以这些对象的关系可以看作是一条链,链头就是变量所处的对象,链尾就是全局对象。

var value = 1;function foo() {console.log(value);
}function bar() {var value = 2;foo();
}bar();// 结果是1

  • 理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题

当JavaScript代码执行的时候回进入不同的执行环境(执行上下文),这些执行环境会构成一个执行环境栈(执行上下文栈)

执行上下文栈是执行上下文的活动记录(数据的出栈和压栈)。

1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象

2. 在全局执行上下文确定后, 将其添加到栈中(压栈)

3. 在函数执行上下文创建后, 将其添加到栈中(压栈)

4. 在当前函数执行完后,将栈顶的对象移除(出栈)

5. 当所有的代码执行完后, 栈中只剩下在全局执行上下文

Error.captureStackTrace 会捕获堆栈信息, 并在第一个参数中创建 stack 属性来存储捕获到的堆栈信息. 如果提供了第二个参数, 该函数将作为堆栈调用的终点. 因此, 捕获到的堆栈信息将只显示该函数调用之前的信息.


  • this的原理以及几种不同使用场景的取值

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.让这些变量的值始终保持在内存中。

实际应用:防抖和节流就是典型的闭包实际应用,还有IIFE也是一个闭包
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

  • 理解堆栈溢出和内存泄漏的原理,如何防止

程序代码运行需要计算空间,就是栈,就像小学生算术需要一张演算纸一样。//这里不考虑堆,或者堆就是多那几张草稿纸。
每个子函数都需要一些局部变量,这需要在演算纸上占用空间。
程序从栈底开始计算,随着各个子函数的调用(入栈)和返回(出栈),占用的栈资源也不断的增加减少。这个过程如果能可视化,就是那个音乐节奏灯,忽闪忽闪的一会高一会儿低的 节奏灯。

栈溢出的意思就是系统分配的演算纸不够用了,你把数字写到纸外面了。
递归调用容易产生这个,是因为递归的层级是动态的,不像非递归程序, 非递归程序中存在一个最深的函数调用链,只要最深的这个链不栈溢出就可以了,而一般递归无法给出这个保证,若递归层次太深就栈溢出了。尾递归可以解决这个问题。

  • 常见的内存泄露的原因

    1. 全局变量引起的内存泄露
    2. 闭包
    3. 没有被清除的计时器

尾调用:尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录。

尾递归:函数调用自身,称为递归。如果尾调用自身,就称为尾递归。


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和rxjs。


  • 理解模块化解决的实际问题,列举几个模块化方案并理解其中原理

前端模块化主要解决两个问题:命名空间冲突,文件依赖管理

模块化构建工具:webpack/requireJS/seaJS/vite等是用来组织前端模块的构建工具

模块化规范:AMD/CMD/CommonJS/es6模块等等规范,规范了如何来组织你的代码


执行机制

  • 为何try里面放returnfinally还会执行,理解其内部机制

finally是在return后面的表达式运算之后执行的,此时并没有返回运算之后的值,而是把值保存起来,不管finally对该值做任何的改变,返回的值都不会改变,依然返回保存起来的值。也就是说方法的返回值是在finally运算之前就确定了的。

先执行try中的语句,包括return后面的表达式,
有异常时,先执行catch中的语句,包括return后面的表达式,
然后执行finally中的语句,如果finally里面有return语句,会提前退出,
最后执行try中的return,有异常时执行catch中的return。


  • JavaScript如何实现异步编程,可以详细描述EventLoop机制

JavaScript是单线程,实现异步操作的方式借助了浏览器的其他线程的帮助。其他线程通过任务队列(task queue)和事件循环(event loop)。

方法:回调函数、事件监听、发布订阅、ES6中的Promise、Generator和rxjs

任务队列:在JavaScript异步机制中,任务队列就是用来维护异步任务回调函数的队列。这样一个队列用来存放这些回调函数,它们会等到主线程执行完所有的同步函数之后按照先进先出的方式挨个执行。

事件循环:主线程在执行完同步任务之后,会无限循环地去检查任务队列中是否有新的“任务”,如果有则执行。而这些任务包括我们在异步任务中定义的回调函数,也包括用户交互事件的回调函数。通过事件循环,Javascript不仅很好的处理了异步任务,也很好的完成了与用户交互事件的处理。因为在完成异步任务的回调函数之后,任务队列中的任务都是由事件所产生的,因此我们也把上述的循环过程叫做事件循环。


  • 宏任务和微任务分别有哪些

宏任务:包括整体代码script,setTimeout,setInterval,I/O,UI render

微任务:Promise,process.nextTick(类似node.js版的"setTimeout"),MutationObserver

宏任务微任务执行机制


  • 可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法

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] = responseif (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提供的语言规范

ES6入门 --阮一峰


  • 熟练掌握JavaScript提供的全局对象(例如Data、Math)、全局函数、全局属性

全局对象包括:Array、Boolean、Number、String、RegExp、Data、Math、Function、Events

JavaScript全局函数、全局属性


  • 熟练应用map、reduce、filter等高阶函数解决问题

JavaScript Array 对象


  • 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()、test()和match()方法

使用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") 

JS正则表达式完整教程


  • JavaScript异常处理的方式,统一的异常处理方案

JS可以通过 try...catch..finally语句构造+throw运算符 来处理异常

try {// 运行代码[break;]}      catch ( e ) {// 如果发生异常,则运行代码[break;]}[ finally {// 无论如何,始终执行的代码// 异常发生}]

前端工程师自检清单(JavaScript基础)相关推荐

  1. 一名前端工程师自检清单与思考(来吧,干完这套清单年薪30不是梦)

    一份不想成为劝退清单的劝退清单 一.JavaScript基础 变量和类型 1.JavaScript规定了几种语言类型 2.JavaScript对象的底层数据结构是什么 3.Symbol类型在实际开发中 ...

  2. 【GitChat】精选——双 11 大前端工程师读书清单

    GitChat 是一款基于微信平台的 IT 阅读/写作互动产品.我们的目的是通过这款产品改变 IT 知识的学习方式,让专业读者获得自主选择权,让知识分享者获得收益. 关于GitChat 你想知道的都在 ...

  3. 【前端工程师手册】JavaScript作用域拾遗

    [前端工程师手册]JavaScript作用域拾遗 昨天总结了一些作用域的知识[前端工程师手册]JavaScript之作用域,但是发表完发现忘记了一些东西,今天拾个遗. 昨天说到了JavaScript中 ...

  4. 前端工程师就业班Sass基础+进阶+案例开发经验【JS++前端】-艾小野-专题视频课程...

    前端工程师就业班Sass基础+进阶+案例开发经验[JS++前端]-98人已学习 课程介绍         本套课程是摘选自JS++第三期前端工程师精英就业班系列课程,进行系统深度的对Sass技术知识点 ...

  5. 什么才是市场急需的前端工程师?【零基础web前端入门视频教程】

    据统计,国外的前端开发人员和后端开发人员比例约1:1,但是在国内比例却在1:3以下,Web前端开发职位人才缺口巨大.前端工程师的发展之路十分有"钱"景. 每天,HR 群都有人在吐槽 ...

  6. 前端面试知识点总结JavaScript基础之原型和原型链(二)

    一.JavaScript基础 原型和原型链 1.理解原型设计模式以及JavaScript中的原型规则 设计模式 1.工厂模式:在函数内建立一个对象,给对象赋予属性及方法再将对象返回设计模式. func ...

  7. table虚线边框_web前端工程师7天0基础到精通(TABLE+CSS制作《互联世纪网》)

    项目七 项目实践:TABLE+CSS制作<互联世纪网> 实践目标 1. 熟悉CSS属性 2. 熟练运用CSS属性控制网页样式 3. 熟悉网页制作流程 项目简介: 通过上一章节的学习,我们了 ...

  8. 《大学生前端成长记》 ---JavaScript基础 --冒泡(Bubble)和取消冒泡(cancelBubble)

    Javascript的冒泡,原来是这样! 事件传递有两种方式:冒泡与捕获. 冒泡(Bubble) 举个例子,我碰了你的手,你的手属于你,所以我也就碰了你. 就是这么简单,如果还不明白,那就看下面 &l ...

  9. 前端工程师面试题JavaScript部分(第二季)

    哪个公司出的题就表了,关键看题,和alert(1&&2)这种怪题比起来,相对接地气一点 !!(0==false) //!!一般是用来转为布尔值的,0为false,false==fals ...

最新文章

  1. 博客入驻阿里“云栖社区”
  2. java十四章带参方法课后_java14带参的方法
  3. matlab求心率,心电图QRS波检测(计算心跳次数)
  4. CakePHP 2.x十分钟博客教程
  5. 如何用python写数值运算_如何理解Python的数值运算?
  6. C++算法学习(分支限界法)
  7. python判断变量相等_Python判断两个对象相等的原理
  8. 前端开发技巧:网页切图图片格式选择GIF、JPEG 和 PNG区别和对比
  9. c语言输出最大的数ns流程图_图8循环结构的NS流程图.ppt
  10. python循环n次_如何使循环重复n次-Python 3
  11. 【过程挖掘算法3】Heuristic Miner(启发式挖掘算法)
  12. CF1428 G1,G2 . Lucky Numbers题解
  13. iOS 开发:『Runtime』详解(二)Method Swizzling
  14. windbg分析C++ EH exception
  15. k30pro杀进程严重怎么解决_命运2掉帧严重怎么解决?GoLink免费加速器助力玩家稳定畅玩...
  16. 火狐浏览器打开后是搜狗浏览器_搜狗浏览器和Firefox浏览器哪个好
  17. 美团拍店,一个“顺道”赚钱的小项目,去饭店的路上,饭钱有了
  18. js利用tab键切换当前页面_JavaScript跳转到指定页面并且到指定的tab切换窗口
  19. Flink之窗口 (Window) 下篇
  20. BUMO:超级节点共建接口

热门文章

  1. Activity Diagram(活动图)几个重要节点
  2. 如何使用MATLAB语言读写YUV文件
  3. 邯郸市高新技术企业申报奖励补助以及认定条件细则
  4. KesionIMALL 电子商务系统源码
  5. java 方法 示例_Java BigDecimaldividAndRemainder()方法与示例
  6. SAPIEN PowerShell最高编辑器和工具之一
  7. IT忍者神龟之css规范
  8. 小红书招聘实习生啦!可转正!
  9. Office 2019 for Mac激活失败,显示未找到许可证怎么办?
  10. 【安信可PB-01/02模组专题③】ESP32-G WIFI蓝牙网关与PB02模组开发进行组网通讯