对前端的了解
现如今前端可谓是包罗万象,各种高大上的基础库和框架,极具效率的构建工具,还有近几年流行的微信小程序等等。一些一两个文件的项目并非是前端技术的主要应用场景,更具商业价值的则是复杂的Web应用,它们功能完善,界面繁多,为用户提供了完整的产品体验。从本质上,所有的Web应用都是一种运行在网页浏览器中的软件,这些软件的图形用户界面(简称GUI),即为前端。由于Web应用的复杂程度与日俱增,用户对其前端界面也提出了更高的要求,Web前端开发开始趋于一种工程化。
前端工程开发大致分为四个阶段:
第一阶段:库/框架的选型
现在基本没有人完全从0开始做网站,vue/react等框架横空出世,解放了不少生产力,合理的技 术选型可以为项目节省许多工程量,这点毋庸置疑。在框架选择的时候,要考虑到团队成员的技术栈,作者对文档的维护程度,还有个人对框架原理的理解等等。
第二阶段:简单构建优化
选型之后基本就可以开始敲代码了,不过光解决开发效率还不够,必须要兼顾运行性能。第二阶段会选型一种构建工具,对代码进行压缩,校验,之后再以页面为单位进行简单的资源合并。
第三阶段:JS/CSS模块化开发
分而治之是软件工程中的重要思想,是复杂系统开发和维护的基石,这点放在前端开发中同样适用。在解决了基本开发效率和运行效率之后,就要开始思考维护效率,模块化是目前前端最流行的分治手段。
JS模块化方案:AMD/CommonJS/ES6 Module CSS模块化开发:less、sass、stylus等预处理器的import/mixin特性支持下实现的
第四阶段:组件化开发与资源管理
组件化开发
前端作为一种GUI软件,光有JS/CSS的模块化还不够,对于UI组件的分治也有着同样迫切的需求。
前端组件化开发理念,简单解读一下:
- 页面上的每个 独立的 可视/可交互区域视为一个组件;
- 每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护;
- 由于组件具有独立性,因此组件与组件之间可以 自由组合;
- 页面只不过是组件的容器,负责组合组件形成功能完整的界面;
- 当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换。
资源管理
模块化/组件化开发之后,我们最终要解决的,就是模块/组件加载的技术问题。然而前端与客户端GUI软件有一个很大的不同:前端是一种远程部署,运行时增量下载的GUI软件。
前端应用没有安装过程,其所需程序资源都部署在远程服务器,用户使用浏览器访问不同的页面来加载不同的资源,随着页面访问的增加,渐进式的将整个程序下载到本地运行,“增量下载”是前端在工程上区别于客户端GUI软件的根本原因。
根据“增量”的原则,我们应该精心规划每个页面的资源加载策略,使得用户无论访问哪个页面都能按需加载页面所需资源,没访问过的无需加载,访问过的可以缓存复用,最终带来流畅的应用体验。这正是Web应用“免安装”的魅力所在。
由“增量”原则引申出的前端优化技巧几乎成为了性能优化的核心,有加载相关的按需加载、延迟加载、预加载、请求合并等策略;有缓存相关的浏览器缓存利用,缓存更新、缓存共享、非覆盖式发布等方案;还有复杂的BigRender、BigPipe、Quickling、PageCache等技术。这些优化方案无不围绕着如何将增量原则做到极致而展开。
参考自前端工程——基础篇
JS三座大山
1.原型和原型链
原型五大规则
- 1.所有引用类型(函数 对象 数组),都具有对象特性,即可自由扩展属性
- 2.所有JS对象都有 proto 属性(隐式原型),属性值是一个普通对象
- 3.函数都有prototype属性(显式原型),除了Function.prototype.bind(),属性值也是一个普通对象
- 4.对象的 proto 属性指向其构造函数的prototype属性值 (obj._ proto === Obj.prototype )
- 5.当试图得到对象的某个属性时,如果此对象本身没有这个属性,那么会去其 proto (即其构造函数的prototype)中寻找
原型链
- 简单来说,就是 proto 将对象和原型连接起来。
原型继承和Class继承
组合继承和寄生组合继承是原型继承的两种方式
组合继承
实现
- 在子类的构造函数里面调用Parent.call(this,value)继承父类的属性
- 2.将子类的prototype指向new Parent()构造出来的实例,继承父类的函数
缺点
继承父类函数的时候,调用了父类的构造函数,导致子类原型上多了不必要的父类属性,存在内存上的浪费
1
2
3
4
5
6
7
8
9
10
11function Parent(val){
this.value=val;
}
Parent.prototype.getValue=funtion(){
console.log(this.value);
}
function Child(val){
Parent.call(this,val);
}
Child.prototype=new Parent();
//Child.prototype._proto_ ===Parent.prototype
寄生组合继承
实现
- 优化掉组合继承的缺点,关键在继承父类函数这一步
优点
既解决了无用父类属性的问题,还能正确找到子类的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function Parent(val){
this.value=val;
}
Parent.prototype.getValue=function (){
console.log(this.value);
}
function Child(val){
//继承父类属性
Parent.call(this,val);
}
Child.prototype=Object.create(Parent.prototype,{
constructor:{
value:Child,
enumerable:false,
writable:true,
configurable:true
}
});//Object.create()方法创建一个新对象,使用现有对象来提供新建对象的__proto__
Class继承
Class的本质就是函数,它是一种语法糖
实现核心是extends表明继承哪个类,还有在子类构造函数中调用super函数
1
2
3
4
5
6
7
8
9
10
11
12
13Class Parent{
constructor(val){
this.value=val;
}
getValue(){
console.log(this.value);
}
}
Class Child extends Parent{
constructor(val){
super(val);
}
}
2.作用域和闭包
前置知识:尽管通常将JS归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。
某个方法或代码块运行特别频繁时,这些代码被认定为“热点代码(Hot Spot Code)”,然后这些代码会被编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为:即时编译器(Just In Time Compiler,JIT)。JIT一般用于将字节码编译为机器码。JIT编译器是“动态编译器”的一种,相对的“静态编译器”则是指的比如:C/C++的编译器。
v8引入了JIT在运行时把js代码进行转换为机器码。这里的主要区别在于V8不生成字节码或任何中间代码。
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
分词/词法分析(Tokenizing/Lexing)
将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。例如,var a=2; 这段程序通常会被分解为这些语法单元:var 、a、=、2
解析/语法分析(Parsing)
将词法单元流(数组)转换成“抽象语法树”(AST)。
代码生成
将AST转化为可执行代码的过程。简单的说,就是将var a=2;的AST转化为一组机器指令,用来创建一个叫作a的变量(包括分配内存等),并将一个值储存在a中。
比起那些编译过程只有三个步骤的语言的编译器,JS引擎要复杂得多,它在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。(垃圾回收机制)
划重点:任何JS代码片段在执行前都要进行编译(通常就在执行前的几微妙,甚至更短)。而解释型语言是在运行时才转换成机器可以理解的语言执行。
作用域
- 定义:简单的说,就是根据名称查找变量的一套规则。详细的说,就是负责收集并维护由所有标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
- 相关知识
- 引擎(V8引擎/JS引擎):从头到尾负责整个JS程序的编译及执行过程
- 编译器(JIT):负责语法分析以及代码生成等
- 关于变量的赋值操作(var a=2),执行两个动作
- 编译器在当前作用域中声明一个变量(如果之前没有声明过)
- 运行时引擎会在作用域中查找变量,如果找到就会对它赋值。(查找分为LHS和RHS查询)
作用域链
- 定义:当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套,形成了作用域链。或者说,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级——>定义时的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象(VO)构成的链表就叫做作用域链。
- 遍历嵌套作用域链的规则:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
LHS和RHS
LHS:赋值操作的目标是谁,主要用来赋值。RHS:谁是赋值操作的源头,主要用来取值。
异常情况
RHS:如果在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError的异常。如果找到了一个变量,但是对这个变量的值进行了不合理的操作,比如试图对一个非函数类型的值进行函数调用,或者引用null或undefined类型的值中的属性,那么引擎会抛出TypeError的异常。
ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作是非法或者不合理的。
LHS:在非“严格模式”下,执行LHS查询,如果在顶层(全局作用域)中无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎。
预编译
- 我们习惯将var a=2;看作一个声明,而实际上JS引擎并不那么认为,它将var a和a=2当作两个单独的声明,第一个定义声明是在编译阶段进行,第二个赋值声明会被留在原地等待执行阶段。
- 变量提升:在编译阶段,无论作用域中的变量出现在什么地方,所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为“提升”。
- 每进入到一个不同的运行环境(全局环境、函数环境)都会创建 一个相应的执行上下文(execution context,EC),那么在一段js程序中一般都会创建多个执行上下文,js引擎会以栈的数据结构对这些执行进行处理,形成执行上下文栈(ECStack),栈底永远是全局执行上下文(global execution context),栈顶则永远是当前的执行上下文。
- 执行上下文(Execution Context EC):执行的基础设施
- 定义:JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下
文”。 - 组成历史
- ES3
- Variable Object(VO) : 变量(variables),函数声明(function declaration),函数形参(arguments)[ 全局:GO,函数:AO]
- [[Scope属性]] :指向作用域链,主要用于变量查找
- this value:指向一个环境对象,是调用当前可执行代码的对象的引用
- ES5
- lexical environment:词法环境,当获取变量时使用。由两部分组成,包括环境记录(enviroment record):存储变量和函数声明和对外部环境的引用(outer):可以通过它访问外部词法环境。
- variable environment:变量环境,当声明变量时使用。变量环境也是个词法环境,主要的区别在于lexicalEnviroment用于存储函数声明和变量( let 和 const )绑定,而variableEnviroment仅用于存储变量( var )绑定。
- this value:this 值。
- ES2018
- lexical environment:词法环境,当获取变量或者 this 值时使用。
- variable environment:变量环境,当声明变量时使用
- code evaluation state:用于恢复代码执行位置。
- Function:执行的任务是函数时使用,表示正在被执行的函数。
- ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
- Realm:使用的基础库和内置对象实例。
- Generator:仅生成器上下文有这个属性,表示当前生成器。
- ES3
- 定义:JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下
- 作用域 VS 执行上下文
- JavaScript 采用的是词法作用域(lexical scoping),也就是静态作用域,函数的作用域在函数定义的时候就决定了。
- 两者的关系
- 存储关系,执行上下文中的[[Scope]]属性存储了当前作用域
- 执行上下文在运行时确定,是动态的,随时会变;作用域是在定义时确定,永远不会变
- Variable Object(VO) 变量对象的创建
- 1.函数的所有形参
- 由名称和对应值组成的一个变量对象的属性被创建(实参和形参相统一)
- 没有实参,属性值设为 undefined
- 2.函数声明(函数声明优先)
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
- 3.变量声明(没有通过 var 关键字声明,所以不会被存放在 AO 中)
- 由名称和对应值(undefined)组成一个变量对象的属性被创建
- 如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性
- 1.函数的所有形参
PS:1.GO可以通过 this 引用,在客户端 JavaScript 中,GO就是 Window 对象
console.log(window===this);//true
2.客户端 JavaScript 中,GO( this 引用) 有 window 属性指向自身
console.log(this.window===this);//true
函数执行上下文中作用域链和变量对象的创建过程
- 1.函数创建/定义:保存作用域链到函数内部属性[[scope]]中(作用域链就是父级VO的层级链,是定义时的父级,而不是执行时的)
- 2.函数执行前(预编译)
- 2.1 当前函数上下文压入执行上下文栈(ECStack)
- 2.2 复制函数内部属性[[scope]] 到函数上下文的[[scope]]属性中
- 2.3 创建AO对象
- 2.4 将当前AO对象压入作用域链顶端
- 3.函数代码执行时:修改AO属性的值,变量查找
- 4.函数执行完毕,函数上下文出栈
闭包
定义:闭包其实只是一个绑定了执行环境的函数,换言之,当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
组成部分
- 环境部分
- 环境:函数的词法环境(执行上下文的一部分)
- 标识符列表:函数中用到的未声明的变量
- 表达式部分:函数体
- 环境部分
闭包的用途
- 1.可以读取函数内部的变量
- 2.让这些变量的值始终保持在内存中 ——>涉及到垃圾回收机制
应用场景
- 模块。两个必要条件:1.必须有外部的封装函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。2.封装函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
栗子~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function CoolModule(){
var something="cool";
var another = [1,2,3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another.join("!"));
}
return {
doSomething:doSomething,
doAnother:doAnother
};
}
var foo = CoolModule();
foo.doSomething();//cool
foo.doAnther();//1!2!3
this
关键:this是执行上下文的组成部分,上下文在代码执行时才确定,所以this要在执行时才能确认值,定义时无法确认。(this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用)
使用场景:
1.箭头函数:this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。
栗子~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo(){
return (a) =>{
//this继承自foo()
console.log(this.a);
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar=foo.call(obj1);
bar.call(obj2);//2,不是3!
/* 代码解析:foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar的this也会绑定到obj1。箭头函数没有this指向,对其使用call绑定无效。*/
* 2.构造函数(new绑定):被固化在实例上
* 3.bind、apply、call(显式绑定):取决于第一个参数
* 4.对象属性(隐式绑定):指向调用的对象
* 隐式丢失:被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定。
栗子~
1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(){
console.log(this.a);
}
var obj={
a:2,
foo:foo
};
var bar = obj.foo;//函数别名
var a = "oops,global";//a是全局对象的属性
bar();//"oops,global"
/* 代码解析:虽然bar是obj.foo的一个引用,但是实际上,它引用foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。 */
* 5.普通函数(默认绑定):作为独立函数调用,指向window
3. 异步和单线程
前置知识:
宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。
在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列。Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API,则会添加宏观任务。
JavaScript 引擎等待宿主环境分配宏观任务,在操作系统中,通常等待的行为都是一个事件循环,所以在 Node 术语中,也会把这个部分称为事件循环
- 宏任务(macroTask) : script、setTimeout、setInterval、setImmediate、I/O、UI rendering
- 微任务(microTask) : promise、process.nextTick、MutationObserver
可以把 JavaScript 程序写在单个 .js 文件中,但是这个程序几乎一定是由多个块构成的。这些块中只有一个是现在执行,其余的则会在将来执行。最常见的块单位是函数。
程序中将来执行的部分并不一定在现在运行的部分执行完之后就立即执行。换句话说,现在无法完成的任务将会异步完成,因此并不会出现人们本能地认为会出现的或希望出现的阻塞行为。
程序的一部分现在运行,而另一部分则在将来运行——现在和将来之间有段间隙,在这段间隙中,程序没有活跃执行。
所有重要的程序(特别是 JavaScript 程序)都需要通过这样或那样的方法来管理这段时间间隙,这时可能是在等待用户输入、从数据库或文件系统中请求数据、通过网络发送数据并等待响应,或者是在以固定时间间隔执行重复任务(比如动画)。在诸如此类的场景中,程序都需要管理这段时间间隙的状态。地铁门上不也总是贴着一句警示语——“小心空隙”(指地铁门与站台之间的空隙)。
异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。
- 单线程
所谓单线程,是指在JS引擎中负责解释和执行JS代码的线程只有一个,不妨叫它主线程。实际上还存在其他线程,比如处理Ajax请求的线程、处理DOM事件的线程、定时器线程等等,这些可以称之为工作线程。
消息(Task) 队列和事件循环(Event Loop)
- 消息队列:一个先进先出的队列,里面存放着各种消息。消息就是注册异步任务时添加的回调函数。
- 事件循环:主线程重复从消息队列中取消息、执行的过程。
- 浏览器中的Event Loop
- 执行顺序:主线程(同步代码)——>微任务——>宏任务
- 当执行JS代码时,遇到异步代码,会被挂起并在需要执行的时候放入Task队列中。一旦执行栈为空,Event Loop 会从Task队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说JS中的异步还是同步行为
- Node中的Event Loop
- 六个阶段:timers,pending callbacks,idle、prepare,poll,check,close callbacks
- 执行顺序:按阶段顺序执行,并且每一阶段结束都要查询是否存在微任务,若存在,则全部执行(nextTick优先)
- timers 执行setTimeout、setInterval
- poll
- 1.回到timers阶段执行回调
- 2.执行I/O回调
- check 执行setImmediate
Promise
- Promise 翻译过来有承诺的意思,这个承诺在未来会有一个确切的回复,并且该承诺有三种状态,分别是:1.pengding(等待中) 2.resolved(完成了) 3.rejected(拒绝了)
注意:这个状态一旦从等待状态变为其他状态就永远无法更改了
- 我们在构造 Promise 的时候,构造函数内部的代码是立即执行的
- 链式调用:每次调用then之后返回的都是一个Promise,并且是一个全新的Promise。如果在then中使用了return,那么return的值会被Promise.resolve()封装
- 缺点:1.无法取消Promise(Promise 的设计就是一个状态机,pending 到 resolve / reject 的状态变换是单向且唯一的,没有所谓的 cancel 状态) 2.错误需要通过回调函数捕获
- Promise 翻译过来有承诺的意思,这个承诺在未来会有一个确切的回复,并且该承诺有三种状态,分别是:1.pengding(等待中) 2.resolved(完成了) 3.rejected(拒绝了)
generator
- generator函数是异步操作的一个容器,在实例化后并没有立即执行,而是返回一个迭代器。
- CO库就是在恰当的时候执行这些操作,基于Promise实现的。generator经常搭配CO一起使用,要求yield是thunk函数或者Promise。
- next执行时传入的参数是上一个yield的返回值,如果不传参,yield永远返回undefined
async/await
外异内同:async关键字声明了一个异步函数,函数体内await语句是同步执行的。
async:返回的是一个Promise对象。当async函数返回一个值时,会被Promise.resolve()封装
await +Promise/值:等待一个表达式,该表达式返回Promise.resolve()中的参数或者一个具体的值。
缺点:await将异步代码改造成了同步代码,如果多个异步代码没有依赖却使用了await会导致性能降低。可以使用Promise.all来解决。
JS其它重要知识
内存泄漏
- 定义:不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
- 识别方法:经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。 这就要求实时查看内存的占用情况。
- 浏览器(Chrome)
- 打开开发者工具,选择 Performance 面板
- 在顶部勾选 Memory
- 点击左上角的 record 按钮
- 在页面上进行各种操作,模拟用户的使用情况
- 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况
- 服务器
- 使用 Node 提供的 process.memoryUsage 方法查看内存情况
- 浏览器(Chrome)
- 案例
- 意外的全局变量
- 被遗忘的定时器和回调函数
- 闭包
- DOM引用(WeakMap解决)
垃圾回收机制(Garbage Collection,GC 算法)
- V8为方便实现垃圾回收,将堆分为两个生代:
- 新生代:为新建的对象分配空间,新生代中的对象一般存活时间较短,经常需要进行垃圾回收。其内存空间被分为两半,一半是From空间,另一半是To空间。新创建的对象会被放入From空间中,当From空间被占满时,新生代GC就会启动。算法会检查From空间中存活的对象并复制到To空间,如果有失活的对象就销毁,复制完成后,将From空间和To空间互换,这样GC就结束了。
- 老生代:老生代中的对象一般存活时间较长且数量也多。一般使用标记清除算法和标记压缩算法。
- 标记清除(Mark-Sweep):分为标记和清除两个阶段。在标记阶段需要遍历堆中的所有对象,并标记活着的对象,然后进入清除阶段。在清除阶段,只清除那些没有被标记的对象(即失活对象)。该算法有个缺点,就是一次标记清除后,内存空间往往是不连续,会产生很多的内存碎片。
- 标记压缩(Mark-Compact):此算法是为了解决内存碎片的问题而出现的。将清除阶段变为压缩阶段,将活着的对象向内存区的一端移动,直到所有对象都移动完成后清理掉不需要的内存。缺点是涉及到对象的移动,所以效率不是很高。
- 对象的晋升(新生代——>老生代)
- 1.对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否经历过一次新生代的清理。如果是,则晋升到老生代中。
- 2.对象从From空间复制到To空间时,如果To空间已被使用超过25%,那么这个对象将直接晋升到老生代。
深浅拷贝
浅拷贝
- 浅拷贝只是拷贝基本类型的数据,如果要复制对象的属性是引用类型,那么拷贝的是地址
- Object.assign 和 …扩展运算符可以实现
深拷贝
深拷贝可以拷贝引用类型的数据
通过JSON.parse(JSON.stringify(object))解决,反序列化和序列化
- 局限性
- 会忽略 undefined、symbol
- 不能序列化函数
- 不能解决循环引用的问题
- 局限性
通过MessageChannel实现,可以处理undefind和循环引用对象,但还是不能解决函数的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function structuralClone(obj){
return new Promise((resolve,reject) =>{
let { port1,port2 }=new MessageChannel();//解构赋值
port2.onmessage=(ev)=>{ resolve(ev.data)};
port1.postMessage(obj);
})
}
var obj={
a:1,
b:{
c:2
},
d:undefined
}
obj.b.e=obj.b;
const test=async ()=>{
const clone=await structuralClone(obj);//await等待resolve中的参数值
console.log(clone);
}
test();
模块化
好处
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
立即执行函数
- 通过函数作用域,解决了命名冲突、污染全局作用域的问题
AMD/CMD 主要用于浏览器
- Asynchronous Module Definition(AMD 异步模块定义) RequireJS在推广过程中对模块定义的规范化产出。特点是依赖前置,会先尽早地执行依赖
- Common Module Definition(CMD 公共模块定义) SeaJS在推广过程中对模块定义的规范化产出。特点是依赖就近,延迟执行。
CommonJS 主要用于服务器
- NodeJS是CommonJS规范的实现,webpack也是以CommonJS的形式写的
ES6 Module 浏览器和服务器通用的模块解决方案
两个命令构成:import 和 export
ES6 Module 与 CommonJS的区别
- 前者不支持动态导入,后者支持
- 前者是异步导入,因为用于浏览器,需要下载文件,如果采用同步导入会对渲染有很大影响; 后者是同步导入,因为用于服务器,文件都在本地
- 前者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会随导出值变化;后者在导出时都是值拷贝,就算导出值改变了,导入值也不会改变。如果想更新值,就要重新导入一次
- ES Module 会编译成 require/exports 来执行
- 变量
- 不允许意外创建全局变量
message="Hello world"
会抛出ReferenceError - 不能对变量使用delete操作符
- 对变量名也有限制,不能使用保留字
- 不允许意外创建全局变量
- 对象
- 重名属性会报错
- 函数
- 重名参数会报错
- 修改命名参数的值不会反映到 arguments 中
- 不能访问 arguments.callee 和 arguments.caller
- if 语句中声明函数会报错
- eval()
- 它在包含上下文中不再创建变量或者函数,但是在被求值的特殊作用域中是有效的
- eval 和 arguments
- 禁止使用 eval 和 arguments 作为标识符,也不允许读写它们的值
- 抑制 this
- 使用函数 apply 和 call 时,不能传入null
- 其他变化
- 去掉了JS中的以0开头的八进制字面量
浏览器相关
- 事件流的三个阶段(事件触发的三个阶段)
- 捕获阶段 事件从window传递到目标
- 命中阶段 事件已经到达目标
- 冒泡阶段 事件从目标传达到window
- 注册事件
- 通常使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值或者对象。
- 对于布尔值参数useCapture来说,默认值是false。对于对象参数来说,可以使用以下几个属性:capture——布尔值、once——布尔值,为true时,表示该回调只会调用一次,调用以后会清除监听、passive:布尔值,表示永远不会调用preventDefault
- 事件代理
- 原理: 事件冒泡
- 一个节点的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上
- 优点
- 节省内存(绑定一次事件和绑定一千次是不一样的)
- 不需要给子节点注销事件
- JSONP
- 原理:利用< script >标签没有跨域限制的漏洞
- 优缺点:兼容性很好,但是只限于Get 请求
- CORS ( Cross-origin resource sharing ) 跨域资源共享
- 实现CORS通信的关键是后端。服务器设置Access-Control-Allow-origin就可以开启CORS。
- 简单请求
- 方法
- GET
- POST
- HEAD
- Content-Type
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 方法
- 复杂请求
- 不符合简单请求条件的请求,首先会发起一个预检请求,该请求是OPTIONS方法的
- postMessage
- 一个页面发送消息,另一个页面判断来源并接收消息
- 可以搭配 MessageChannel实现
- document.domain
- 限制 只有在一级域名相同的时候才能使用,要把document.domain设置成自身或更高一级的域名
- cookie 、localStorage、sessionStorage、indexDB
- 数据生命周期
- cookie 一般由服务器生成,可以设置过期时间
- localStorage 除非被清理,否则一直存在
- sessionStorage 页面关闭就清理
- indexDB 除非被清理,否则一直存在
- 数据存储大小
- 4K 5M 5M 无限
- 与服务端通信
- cookie 每次请求都会携带在header中,影响请求性能
- 其他的都不参与
- 数据生命周期
- cookie安全性
- http-only 属性值设置为true,则不能通过JS获取cookie,减少XSS攻击
- secure 属性值设置为true ,则只能在协议为HTTPS的请求中携带
- same-site 属性值设置为true,则规定浏览器不能在跨域请求中携带cookie,减少CSRF攻击
缓存位置
- Service Worker 运行在浏览器背后的独立线程,一般用于实现缓存功能。传输协议必须是HTTPS。优点是可以自由缓存,并且缓存是持续性的
- Memory Cache 内存中的缓存,读取高效,但是持续性很短,会随着进程的释放而释放。一旦我们关闭页面,内存中的缓存也就被释放了。
- Disk Cache 磁盘中的缓存,读取速度慢点,但是较之Memory cache 胜在容量和持续性(存储时效性)上。
- Push Cache HTTP/2中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间很短暂,只在会话中存在,一旦会话结束就被释放。
缓存策略
强缓存和协商缓存,并且都要通过设置HTTP Header 实现
强缓存 表示在缓存期间不需要请求
- Expires HTTP/1 产物 ,受限于本地时间,如果本地时间修改,可能造成缓存失效
- Cache-Control HTTP/1.1 产物 ,优先级高于Expires,可以在请求头或者响应头中设置
协商缓存
- If-Modified-since 和 Last-Modified 有两个弊端
- If-None-Match 和 Etag HTTP/1.1,优先级高于Last-Modified
状态码 200 和 304
F5 刷新和 Ctrl+F5 强制刷新
- F5 刷新 Expires/Cache-Control 无效 ,但是 Last-Modified/Etag 有效 可以进行协商缓存 会出现304状态码
- Ctrl+F5 强制刷新 Expires/Cache-Control 和 Last-Modified/Etag 都无效,需要重新对资源发起请求,会出现200状态码
- 1.HTML 文件——> DOM树
- 字节数据——>字符串——>Token——>Node——>DOM
- 2.CSS 文件 ——> CSSOM树
- 字节数据——>字符串——>Token——>Node——>CSSOM
- 避免写过于具体的CSS选择器
- 3.DOM+CSSOM——>Render Tree
- 为什么操作DOM慢
- DOM属于渲染引擎中的东西,JS属于JS引擎中的东西,JS操作DOM,涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗
- 重绘和回流
- 重绘 不影响布局,只更改节点的外观
- 回流 改变布局或几何属性
- 回流一定会发生重绘,重绘不一定发生回流
- 减少回流和重绘
- 使用transform 替代 top
- 使用 visibility 替换 display:none
- CSS 选择器从右往左匹配查找,避免层级过多
- 将频繁重绘或者回流的节点设置为图层
- 事件流的三个阶段(事件触发的三个阶段)
安全性相关
- 定义 攻击者想尽办法将可执行代码注入到网页中
- 种类 持久型和非持久型
- 持久型 攻击代码被服务端写入数据库
- 非持久型 修改URL参数注入攻击代码
- 防御方法
- 转义字符 使用 js-xss
- CSP(Content-Security-Policy) 本质上就是建立白名单
- 两种方式开启 设置HTTP Header中Content-Security-Policy和设置meta标签中的http-equiv
- 不允许JS代码获取cookie 设置cookie的http-only属性
- 定义 攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。简单的说,就是利用用户的登录态发起恶意请求。
- 防御方法
- 不让第三方网站访问到用户Cookie 设置Cookie的same-site属性
- 阻止第三方网站请求接口 设置HTTP Header中的Referer字段,它记录了HTTP请求的源地址
- 请求时附带验证信息,比如验证码或token 服务器下发一个随机Token,每次请求都将Token带上,服务器验证Token是否有效
- 定义 又称界面伪装攻击,是一种视觉上的欺骗手段
- 防御
- HTTP 响应头 ——X-FRAME-OPTIONS
- DENY 不允许通过iframe的方式展示
- SAMEORIGIN 同域名下通过iframe展示
- ALLOW-FROM 指定来源的iframe展示
- JS防御
- HTTP 响应头 ——X-FRAME-OPTIONS
- 定义 攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但实际上整个通信过程都被攻击者控制了。
- 防御 增加一个安全通道来传输信息,比如可以利用HTTPS协议
跨标签页通讯
- 解决方案
- 使用
window
对象; postMessage
API;- 使用
cookies
; - 使用
localStorage
;
- 使用
- 解决方案
ES6
- var 声明变量,会发生变量提升 ——> 执行上下文中VO对象的创建
- let 声明的变量具有块作用域的特征,不能重复声明,不存在变量提升,存在暂时性死区(TDZ)
- const 声明的变量具有let声明的特点,还具有不能修改的特性。要注意的是声明的是值类型还是引用类型,引用类型不变的是地址,并不是其内部的内容
- 所有Symbol()生成的值都是独一无二的,可以从根本上解决对象属性太多导致属性名冲突覆盖的问题。对象中Symbol()属性不能被for…in遍历,但是也不是私有属性。可以用Reflect.ownKeys()遍历。
- 模板字符串
- includes、startsWith、endsWith、repeat、padStart、padEnd
- 解构赋值 不用再写很多let或者var
- …扩展运算符
Array.prototype.flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
- 在Number原型上新增了isFinite(), isNaN(),parseInt(),parseFloat()方法,用来取代传统的全局条件下的这些函数
Number.isInteger()
用来判断一个数值是否为整数- 安全整数和 Number.isSafeInteger()
- Math.trunc 去除一个数的小数部分,返回整数部分
- Math.cbrt 求立方根;Math.hypot 方法返回所有参数的平方和的平方根
- ES6可以直接以变量形式声明对象属性或者方法
- 解构赋值,…扩展运算符
- 在Object原型上新增了is()方法,做两个目标对象的相等比较,用来完善’===’方法。’===’方法中
NaN === NaN //false
其实是不合理的,Object.is修复了这个小bug。 - 在Object原型上新增了assign()方法,用于对象新增属性或者多个对象合并。(浅拷贝)
- ES6在Object原型上新增了getPrototypeOf()和setPrototypeOf()方法
- 箭头函数 ——> this的指向
- ES6新增了双冒号运算符,用来取代以往的bind,call,和apply。
- foo::bar;
// 等同于
bar.bind(foo);
- Set是ES6引入的一种类似Array的新的数据结构。区别是Set实例的成员都是唯一,不重复的。
- Map是ES6引入的一种类似Object的新的数据结构。对象的key不再局限于字符串,也可以是Object。
- Vue 3.0 响应式实现
- 异步、单线程、事件循环
- 继承、原型和原型链
- 模块化