# ES6-ES13
ECMA新描述概念 ECMA新描述概念
词法环境
词法环境是一种规范类型,用于在词法嵌套结构中定义关联的变量、函数等标识符;
一个词法环境是由环境记录(Environment Record)和一个外部词法环境(oute;r Lexical Environment)组成;
一个词法环境经常用于关联一个函数声明、代码块语句、try-catch语句,当它们的代码被执行时,词法环境被创建出来;
也就是在ES5之后,执行一个代码,通常会关联对应的词法环境;
LexicalEnvironment用于处理let、const声明的标识符:
VariableEnvironment用于处理var和function声明的标识符:
环境记录
在这个规范中有两种主要的环境记录值:声明式环境记录和对象环境记录。
声明式环境记录:声明性环境记录用于定义ECMAScript语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与ECMAScript语言值关联起来的Catch子句。
对象式环境记录:对象环境记录用于定义ECMAScript元素的效果,例如WithStatement,它将标识符绑定与某些对象的属性关联起来。
let/const的使用 let/const的使用
let/const无作用域提升
let、const 和 var 的另一个重要区别是作用域提升:
我们知道 var 声明的变量是会进行作用域提升的;
但是如果我们使用 let 声明的变量,在声明之前访问会报错;
1 2 console .log (foo) let foo = "foo"
那么是不是意味着foo变量只有在代码执行阶段才会创建的呢?
事实上并不是这样的,我们可以看一下 ECMA262 对 let 和 const 的描述;
这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值;
从上面我们可以看出,在执行上下文的词法环境创建出来的时候,变量事实上已经被创建了,只是这个变量是不能被访问 的。
那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?
事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解;
作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;
在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;
所以我的观点是 let、const 没有进行作用域提升,但是会在解析阶段被创建出来 。
暂时性死区
我们知道,在let、const定义的标识符真正执行到声明的代码之前,是不能被访问的
从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(TDZ,temporal dead zone)
1 2 3 4 { console .log (name) let name = "why" }
使用术语 “temporal” 是因为区域取决于执行顺序(时间),而不是编写代码的位置;
1 2 3 4 5 function foo ( ){ console .log (message) } let message = "Hello World" foo ()
window对象添加属性
var 的作用域
在我们前面的学习中,JavaScript只会形成两个作用域:全局作用域和函数作用域 。
ES5中放到一个代码中定义的变量,外面是可以访问的:
let/const的块级作用域
在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的:
使用 let 和 const 声明的变量是有块级作用域的
1 2 3 4 5 6 7 8 9 10 { let foo = "foo" function bar ( ) { console .log ("bar" ) } class Person { } } bar ()
但是我们会发现函数拥有块级作用域,但是外面依然是可以访问的:
这是因为引擎会对函数的声明进行特殊的处理,允许像 var 那样进行提升;
块级作用域的应用
1 2 3 4 5 6 const btnEl = document .querySelectorAll ("button" ) for (let i = 0 ; i < btnEl.length ; i++) { btnEl[i].onclick = () => { console .log (i) } }
var/let/const的选择
那么在开发中,我们到底应该选择使用哪一种方式来定义我们的变量呢?
对于var的使用:
我们需要明白一个事实,var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题;
其实是JavaScript在设计之初的一种语言缺陷;
当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解;
但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量 了;
对于let、const:
对于let和const来说,是目前开发中推荐使用的;
我们会优先推荐使用const,这样可以保证数据的安全性不会被随意的篡改;
只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let;
这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;
模板字符串
在ES6之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的(ugly)。
ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接:
首先,我们会使用 `` 符号来编写字符串,称之为模板字符串;
其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容;
1 2 3 4 5 const name = "why" const age = 18 const info = "my name is" + name + ", age is " + ageconsole .log (info);
1 2 3 4 5 const name = "why" const age = 18 const info = `my name is ${name} , age is ${age} ` console .log (info)
标签模板字符串使用
模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。
我们一起来看一个普通的JavaScript的函数:
如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:
模板字符串被拆分了;
第一个元素是数组,是被模块字符串拆分的字符串组合;
后面的元素是一个个模块字符串传入的内容;
1 2 3 4 5 6 7 8 9 function foo (...args ) { console .log ("参数:" , args) } foo ("why" , 18 , 1.88 ) const name = "why" const age = 18 foo `my name is ${name} , age is ${age} , height is ${1.88 } `
箭头函数 箭头函数简介
箭头函数是 ES6 之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:
箭头函数 不会绑定 this 、arguments 属性
箭头函数 不能作为构造函数来使用 (不能和 new 一起来使用,会抛出错误);
箭头函数的格式 1 2 3 4 5 6 7 8 9 10 11 12 13 function foo1 ( ) {}var foo2 = function (name, age ) { console .log ('函数体代码' , this , arguments ) console .log (name, age) } var foo3 = (name, age ) => { console .log ('箭头函数的函数体' ) console .log (name, age) } foo3 ('why' , 18 )
1 2 3 4 5 6 7 8 9 10 11 var names = ['abc' , 'cba' , 'nba' ]names.forEach ((item, index, arr ) => { console .log (item, index, arr) }) setTimeout (() => { console .log ('setTimeout' ) }, 3000 )
箭头函数的简写 1 2 3 4 5 6 7 8 9 10 11 var names = ['abc' , 'cba' , 'nba' ]var nums = [20 , 30 , 11 , 15 , 111 ]names.forEach ((item ) => { console .log (item) }) var newNums = nums.filter ((item ) => { return item % 2 === 0 }) console .log (newNums)
1 2 3 4 5 6 7 8 9 10 var names = ['abc' , 'cba' , 'nba' ]var nums = [20 , 30 , 11 , 15 , 111 ]names.forEach ((item ) => console .log (item)) var newNums = nums.filter ((item ) => { return item % 2 === 0 }) console .log (newNums)
1 2 3 4 var nums = [20 , 30 , 11 , 15 , 111 ]var newNums = nums.filter ((item ) => item % 2 === 0 )console .log ('newNums:' , newNums)
1 2 3 4 5 var arrFn = ( ) => ['abc' , 'cba' ] var arrFn = ( ) => {} var arrFn = ( ) => ({ name : 'why' }) console .log (arrFn ())
1 2 3 4 5 6 7 var nums = [20 , 30 , 11 , 15 , 111 ]var result = nums .filter ((item ) => item % 2 === 0 ) .map ((item ) => item * item) .reduce ((prevValue, item ) => prevValue + item) console .log (result)
箭头函数的 this 指向
1 2 3 4 5 6 7 function foo ( ) { console .log ('foo' , this ) } foo () foo.apply ('aaa' )
1 2 3 4 5 6 7 8 var bar = ( ) => { console .log ('bar:' , this ) } bar () bar.apply ('aaaa' ) console .log ('全局this:' , this )
1 2 3 4 5 6 7 8 9 10 11 12 13 var obj = { name : 'obj' , foo : () => { var bar = ( ) => { console .log ('bar222:' , this ) } return bar }, } var fn = obj.foo ()fn.apply ('bbb' )
箭头函数中 this 的应用
案例一:调用 obj 对象中的一个方法发起网络请求获取数据,并存放在 obj 的 name 属性中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function request (url, callbackFn ) { var results = ['abc' , 'cba' , 'nba' ] callbackFn (results) } var obj = { names : [], network : function ( ) { request ('/names' , (res ) => { this .names = [].concat (res) }) }, } obj.network () console .log (obj)
案例二:定时器中的 this 的使用
调用 obj 对象中的一个方法发起网络请求获取数据,并存放在 obj 的 name 属性中
1 2 3 4 5 6 7 8 9 10 11 12 13 var obj2 = { data : [], getData : function ( ) { setTimeout (() => { var res = ['abc' , 'cba' , 'nba' ] this .data .push (...res) console .log (this ) }, 1000 ) }, } obj2.getData () console .log (obj2)
常见 this 指向的面试题 面试题一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var name = 'window' var person = { name : 'person' , sayName : function ( ) { console .log (this .name ) }, } function sayName ( ) { var sss = person.sayName sss () person.sayName () person.sayName () ;(b = person.sayName )() } sayName ()
面试题二 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 var name = 'window' var person1 = { name : 'person1' , foo1 : function ( ) { console .log (this .name ) }, foo2 : () => console .log (this .name ), foo3 : function ( ) { return function ( ) { console .log (this .name ) } }, foo4 : function ( ) { return () => { console .log (this .name ) } }, } var person2 = { name : 'person2' }person1.foo1 () person1.foo1 .call (person2) person1.foo2 () person1.foo2 .call (person2) person1.foo3 ()() person1.foo3 .call (person2)() person1.foo3 ().call (person2) person1.foo4 ()() person1.foo4 .call (person2)() person1.foo4 ().call (person2)
面试题三 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 var name = 'window' function Person (name ) { this .name = name ;(this .foo1 = function ( ) { console .log (this .name ) }), (this .foo2 = () => console .log (this .name )), (this .foo3 = function ( ) { return function ( ) { console .log (this .name ) } }), (this .foo4 = function ( ) { return () => { console .log (this .name ) } }) } var person1 = new Person ('person1' )var person2 = new Person ('person2' )person1.foo1 () person1.foo1 .call (person2) person1.foo2 () person1.foo2 .call (person2) person1.foo3 ()() person1.foo3 .call (person2)() person1.foo3 ().call (person2) person1.foo4 ()() person1.foo4 .call (person2)() person1.foo4 ().call (person2)
面试题四 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 var name = 'window' function Person (name ) { this .name = name this .obj = { name : 'obj' , foo1 : function ( ) { return function ( ) { console .log (this .name ) } }, foo2 : function ( ) { return () => { console .log (this .name ) } }, } } var person1 = new Person ('person1' )var person2 = new Person ('person2' )person1.obj .foo1 ()() person1.obj .foo1 .call (person2)() person1.obj .foo1 ().call (person2) person1.obj .foo2 ()() person1.obj .foo2 .call (person2)() person1.obj .foo2 ().call (person2)
函数的参数 默认参数
1 2 3 4 5 6 7 8 9 10 11 12 13 function foo (arg1 ) { arg1 = arg1 ? arg1 : "我是默认值" console .log (arg1) } foo (123 , 321 ) foo () foo (0 ) foo ("" ) foo (false ) foo (null ) foo (undefined )
1 2 3 4 5 6 7 8 9 10 11 12 13 function foo (arg1 ) { arg1 = arg1 || "我是默认值" console .log (arg1) } foo (123 , 321 ) foo () foo (0 ) foo ("" ) foo (false ) foo (null ) foo (undefined )
1 2 3 4 5 6 7 8 9 10 11 12 13 function foo (arg1 ) { arg1 = (arg1 === undefined || arg1 === null ) ? "我是默认值" : arg1 console .log (arg1) } foo (123 , 321 ) foo () foo (0 ) foo ("" ) foo (false ) foo (null ) foo (undefined )
1 2 3 4 5 6 7 8 9 10 11 12 function foo (arg1 ) { arg1 = arg1 ?? "我是默认值" console .log (arg1) } foo (123 , 321 ) foo () foo (0 ) foo ("" ) foo (false ) foo (null ) foo (undefined )
1 2 3 4 5 6 7 8 9 10 11 function foo (arg1 = "我是默认值" , arg2 = "我也是默认值" ) { console .log (arg1) } foo (123 , 321 ) foo () foo (0 ) foo ("" ) foo (false ) foo (null ) foo (undefined )
默认值也可以和解构一起来使用:
另外参数的默认值我们通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的):
但是JavaScript允许不将其放到最后,但是意味着还是会按照顺序来匹配;
另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了。
1 2 3 4 const obj = { name : "why" }const { name = "kobe" , age = 18 } = obj console .log (name, age);
1 2 3 4 5 6 function foo (obj = { name: "why" , age: 18 } ) { console .log (obj.name , obj.age ) } foo () foo ({ name : "qwe" , age : 20 , height : 1.88 })
1 2 3 4 5 6 function foo ({ name, age } = { name: "why" , age: 18 } ) { console .log (name, age) } foo () foo ({ name : "abc" , age : 19 , height : 1.88 })
1 2 3 4 5 6 function foo ({ name = "why" , age = 18 } = {} ) { console .log (name, age) } foo () foo ({ name : "qwe" , age : 20 , height : 1.88 })
剩余参数
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:
如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
那么剩余参数和arguments有什么区别呢?
剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的;
注意:剩余参数必须放到最后一个位置,否则会报错。
1 2 3 4 function foo ( ) {}console .log (foo.prototype ) console .log (foo.__proto__ )
1 2 3 4 5 6 7 var bar = ( ) => {}console .log (bar.__proto__ === Function .prototype ) console .log (bar.prototype )
展开语法 展开语法
展开语法(Spread syntax):
可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开;
还可以在构造字面量对象时, 将对象表达式按key-value的方式展开;
展开语法的场景:
在函数调用时使用;
在数组构造时使用;
在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性;
注意:展开运算符其实是一种浅拷贝;
数组展开 1 2 3 4 5 6 const names = ["abc" , "cba" , "nba" , "mba" ]const str = "Hello" const newNames = [...names, "aaa" , "bbb" ]console .log (newNames)
1 2 3 4 5 6 7 8 const names = ["abc" , "cba" , "nba" , "mba" ]const str = "Hello" function foo (name1, name2, ...args ) { console .log (name1, name2, args) } foo (...names) foo (...str)
对象展开 1 2 3 4 5 6 7 8 9 10 11 const obj = { name : "why" , age : 18 } function foo (name1, name2, ...args ) { console .log (name1, name2, args) } foo (...obj)
1 2 3 4 5 6 7 8 9 10 11 const obj = { name : "why" , age : 18 } const info = { ...obj, height : 1.88 , address : "广州市" } console .log (info)
深拷贝与浅拷贝 浅拷贝 1 2 3 4 5 6 7 8 9 10 11 const obj = { name : "why" , age : 18 , height : 1.88 , friend : { name : "curry" } } const info1 = obj console .log (info1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const obj = { name : "why" , age : 18 , height : 1.88 , friend : { name : "curry" } } const info2 = { ...obj, } info2.name = "kobe" console .log (info2.name ) console .log (obj.name ) info2.friend .name = "james" console .log (obj.friend .name )
深拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const obj = { name : "why" , age : 18 , height : 1.88 , friend : { name : "curry" } } const info3 = JSON .parse (JSON .stringify (obj)) console .log (info3.friend .name ) info3.friend .name = "james" console .log (info3.friend .name ) console .log (obj.friend .name )
进制与数值的表示 进制的表示 在ES6中规范了二进制和八进制的写法:
1 2 3 4 5 console .log (100 ) console .log (0b100 ) console .log (0o100 ) console .log (0x100 )
数值的表示 另外在ES2021新增特性:数字过长时,可以使用_作为连接符
1 2 3 4 5 6 const money1 = 100_00_00_0000_00_00 console .log (money1) const money2 = 100_0000_0000_0000 console .log (money2)
Symbol的使用 Symbol的基本使用
Symbol是什么呢?Symbol是ES6中新增的一个基本数据类型,翻译为符号。
那么为什么需要Symbol呢?
在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
比如我们前面在讲apply、call、bind实现时,我们有给其中添加一个fn属性,那么如果它内部原来已经有了fn属性了呢?
比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;
Symbol就是为了解决上面的问题,用来生成一个独一无二的值。
Symbol值是通过Symbol函数来生成的,生成后可以作为属性名;
也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值;
Symbol即使多次创建值,它们也是不同的:Symbol函数执行后每次创建出来的值都是独一无二的;
我们也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性;
1 2 3 4 5 6 7 8 9 10 11 12 13 const obj = { name : "why" , fn : "aaa" } function foo (obj ) { obj.fn = function ( ) { } } foo (obj)console .log (obj) console .log (obj.fn )
Symbol作为属性名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const s1 = Symbol (); const obj = { [s1]: "aaa" , } console .log (obj) const s2 = Symbol () obj[s2] = "bbb" console .log (obj) function foo (obj ) { const sKey = Symbol () obj[sKey] = function ( ) { } console .log (obj) delete obj[sKey] console .log (obj) } foo (obj)
1 2 3 4 5 6 7 8 9 10 const s1 = Symbol ()const s2 = Symbol ()const obj = { name : "why" , age : 18 , [s1]: "aaa" , [s2]: "bbb" } console .log (obj)
1 2 3 4 5 6 7 8 9 10 11 12 const s1 = Symbol ()const s2 = Symbol ()const obj1 = {}obj1[s1] = "aaa" obj1[s2] = "bbb" console .log (obj1) const obj2 = {}Object .defineProperty (obj2, s1, { value : "aaa" }) console .log (obj2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const s1 = Symbol ()const s2 = Symbol ()const obj = { name : "why" , age : 18 , [s1]: "aaa" , [s2]: "bbb" } console .log (Object .keys (obj)) console .log (Object .getOwnPropertySymbols (obj)) const symbolKeys = Object .getOwnPropertySymbols (obj)for (const key of symbolKeys) { console .log (obj[key]) }
相同值的Symbol
前面我们讲Symbol的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的Symbol应该怎么来做呢?
我们可以使用Symbol.for方法来做到这一点;
并且我们可以通过Symbol.keyFor方法来获取对应的key;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const s3 = Symbol ("ccc" )console .log (s3.description ) const s4 = Symbol (s3.description ) console .log (s3 === s4) const s5 = Symbol .for ("ddd" )const s6 = Symbol .for ("ddd" )console .log (s5 === s6) console .log (Symbol .keyFor (s5))
Set的使用 Set的基本使用
1 2 3 const set = new Set ()console .log (set)
1 2 3 4 5 6 7 8 9 10 11 12 13 const set = new Set () set.add (10 ) set.add (22 ) set.add (35 ) set.add (22 ) console .log (set) const info = {} const obj = {name : "obj" } set.add (info) set.add (obj) set.add (obj) console .log (set)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const names = ["abc" , "cba" , "nba" , "cba" , "nba" ]const newNames = []for (const item of names) { if (!newNames.includes (item)) { newNames.push (item) } } console .log (newNames)const names = ["abc" , "cba" , "nba" , "cba" , "nba" ]const newNamesSet = new Set (names)const newNames = Array .from (newNamesSet) console .log (newNames)
Set的常见方法
Set常见的属性:
Set常用的方法:
add(value):添加某个元素,返回Set对象本身;
delete(value):从set中删除和这个值相等的元素,返回boolean类型;
has(value):判断set中是否存在某个元素,返回boolean类型;
clear():清空set中所有的元素,没有返回值;
forEach(callback, [, thisArg]):通过forEach遍历set;
另外Set是支持for of的遍历的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const set = new Set ()console .log (set.size ) const obj = { name : "obj" }const info = {}set.add (obj) set.add (info) set.add (100 ) console .log (set) set.delete (obj) console .log (set) console .log (set.has (info)) set.forEach (item => console .log (item)) for (const item of set) { console .log (item) }
WeakSet的使用 WeakSet的使用
和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。
那么和Set有什么区别呢?
区别一:WeakSet中只能存放对象类型,不能存放基本数据类型;
区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
WeakSet常见的方法:
add(value):添加某个元素,返回WeakSet对象本身;
delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型;
has(value):判断WeakSet中是否存在某个元素,返回boolean类型;
1 2 3 4 5 6 7 8 9 10 11 12 13 let obj1 = { name : "why" }let obj2 = { name : "kobe" }let obj3 = { name : "jame" }let arr = [obj1, obj2, obj3]obj1 = null obj2 = null obj3 = null const set = new Set (arr)console .log (set); arr = null
1 2 3 4 5 6 7 8 9 10 11 let obj1 = { name : "why" }let obj2 = { name : "kobe" }let obj3 = { name : "jame" }const weakSet = new WeakSet ()weakSet.add (obj1) weakSet.add (obj2) weakSet.add (obj3) console .log (weakSet);
WeakSet的应用
注意:WeakSet不能遍历
因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。
所以存储到WeakSet中的对象是没办法获取的;
那么这个东西有什么用呢?
事实上这个问题并不好回答,我们来使用一个Stack Overflow上的答案;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const pWeakSet = new WeakSet ()class Person { constructor ( ) { pWeakSet.add (this ) } running ( ) { if (!pWeakSet.has (this )) { console .log ("Type error: 调用的方式不对" ) return } console .log ("running~" ) } } let p = new Person ()p.running () const runFn = p.running runFn ()const obj = { run : runFn }obj.run ()
Map的使用 Map的基本使用
另外一个新增的数据结构是Map,用于存储映射关系。
但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?
事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);
某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;
那么我们就可以使用Map:
1 2 3 4 5 6 7 8 9 const info = { name : "why" }const info2 = { age : 18 }const obj = { address : "北京市" , [info]: "哈哈哈" , [info2]: "呵呵呵" } console .log (obj)
1 2 3 4 5 6 7 const info = { name : "why" }const info2 = { age : 18 }const map = new Map ()map.set (info, "aaaa" ) map.set (info2, "bbbb" ) console .log (map)
Map的常用方法
Map常见的属性:
Map常见的方法:
set(key, value):在Map中添加key、value,并且返回整个Map对象;
get(key):根据key获取Map中的value;
has(key):判断是否包括某一个key,返回Boolean类型;
delete(key):根据key删除一个键值对,返回Boolean类型;
clear():清空所有的元素;
forEach(callback, [, thisArg]):通过forEach遍历Map;
Map也可以通过for of进行遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const info = { name : "why" }const info2 = { age : 18 }const map = new Map ()console .log (map.size ) map.set (info, "cccc" ) map.set (info2, "bbbb" ) console .log (map) console .log (map.get (info)) map.delete (info) console .log (map) console .log (map.has (info2)) map.clear () console .log (map) map.forEach (item => console .log (item)) for (const item of map) { const [key, value] = item console .log (key, value) }
WeakMap的使用 WeakMap的使用
和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。
那么和Map有什么区别呢?
区别一:WeakMap的key只能使用对象,不接受其他的类型作为key;
区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;
WeakMap常见的方法有四个:
set(key, value):在Map中添加key、value,并且返回整个Map对象;
get(key):根据key获取Map中的value;
has(key):判断是否包括某一个key,返回Boolean类型;
delete(key):根据key删除一个键值对,返回Boolean类型;
1 2 3 4 5 6 7 8 9 10 11 let obj1 = { name : "why" }let obj2 = { name : "kobe" }const weakMap = new WeakMap ()weakMap.set (obj1, "aaa" ) weakMap.set (obj2, "bbb" ) obj1 = null obj2 = null
WeakMap的应用
注意:WeakMap也是不能遍历的
没有forEach方法,也不支持通过for of的方式进行遍历;
那么我们的WeakMap有什么作用呢?(后续专门讲解)
ES6 的其他说明
事实上ES6(ES2015)是一次非常大的版本更新,所以里面重要的特性非常多:
除了前面讲到的特性外还有很多其他特性;
Proxy、Reflect,我们会在后续专门进行学习。
并且会利用Proxy、Reflect来讲解Vue3的响应式原理;
Promise,用于处理异步的解决方案
后续会详细学习;
并且会学习如何手写Promise;
ES Module模块化开发:
从ES6开发,JavaScript可以进行原生的模块化开发;
这部分内容会在工程化部分学习;
包括其他模块化方案:CommonJS、AMD、CMD等方案;
ES7-ES13 ES7 Array Includes
在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。
在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true,否则返回false。
指数运算符
在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。
在ES7中,增加了 **
运算符,可以对数字来计算乘方。
ES8 Object values
之前我们可以通过 Object.keys 获取一个对象所有的key
在ES8中提供了 Object.values 来获取所有的value值:
Object entries
通过 Object.entries 可以获取到一个数组,数组中会存放可枚举属性的键值对数组。
可以针对对象、数组、字符串进行操作;
String Padding
某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。
我们简单具一个应用场景:比如需要对身份证、银行卡的前面位数进行隐藏:
Trailing Commas
在ES8中,我们允许在函数定义和调用时多加一个逗号:
Object Descriptors
Object.getOwnPropertyDescriptors :
Async Function:async、await
ES9
Async iterators:后续迭代器讲解
Object spread operators:前面讲过了
Promise finally:后续讲Promise讲解
ES10 flat flatMap
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
注意一:flatMap是先进行map操作,再做flat的操作;
注意二:flatMap中的flat相当于深度为1;
Object fromEntries
在前面,我们可以通过 Object.entries 将一个对象转换成 entries
那么如果我们有一个entries了,如何将其转换成对象呢?
ES10提供了 Object.formEntries来完成转换:
那么这个方法有什么应用场景呢?
rimStart trimEnd
去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢?
ES10中给我们提供了trimStart和trimEnd;
其他内容
Symbol description:已经讲过了
Optional catch binding:后面讲解try cach讲解
ES11 BigInt
在早期的JavaScript中,我们不能正确的表示过大的数字:
大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的。
那么ES11中,引入了新的数据类型BigInt,用于表示大的整数:
空值合并操作符
ES11,Nullish Coalescing Operator增加了空值合并操作符:
可选链
可选链也是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁:
Global This
在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的
比如在浏览器中可以通过this、window来获取;
比如在Node中我们需要通过global来获取;
在ES11中对获取全局对象进行了统一的规范:globalThis
for..in标准化
在ES11之前,虽然很多浏览器支持for…in来遍历对象类型,但是并没有被ECMA标准化。
在ES11中,对其进行了标准化,for…in是用于遍历对象的key的:
其他内容
Dynamic Import:后续ES Module模块化中讲解。
Promise.allSettled:后续讲Promise的时候讲解。
import meta:后续ES Module模块化中讲解。
ES12 FinalizationRegistry
FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调。
FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调。(清理回调有时被称为 finalizer );
你可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值;
WeakRefs
如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用:
如果我们希望是一个弱引用的话,可以使用WeakRef;
其他内容
Numeric Separator:讲过了;
String.replaceAll:字符串替换;
ES13 method.at()
前面我们有学过字符串、数组的at方法,它们是作为ES13中的新特性加入的:
Object.hasOwn()
Object中新增了一个静态方法(类方法): hasOwn(obj, propKey)
该方法用于判断一个对象中是否有某个自己的属性;
那么和之前学习的Object.prototype.hasOwnProperty有什么区别呢?
区别一:防止对象内部有重写hasOwnProperty
区别二:对于隐式原型指向null的对象, hasOwnProperty无法进行判断
class类成员字段
在ES13中,新增了定义class类中成员字段(field)的其他方式:
Instance public fields
Static public fields
Instance private fields
static private fields
static block
Proxy使用详解 监听对象的操作
我们先来看一个需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程
通过我们前面所学的知识,能不能做到这一点呢?
其实是可以的,我们可以通过之前的属性描述符中的存储属性描述符来做到;
左边这段代码就利用了前面讲过的 Object.defineProperty 的存储属性描述符来对属性的操作进行监听。 但是这样做有什么缺点呢?
首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。
我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty是无能为力的。
所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。
Proxy基本使用
在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:
也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);
之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;
我们可以将上面的案例用Proxy来实现一次:
首先,我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler;
const p = new Proxy(target, handler)
其次,我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听;
Proxy的set和get捕获器
Proxy的所有捕获器
construct和apply
当然,我们还会看到捕捉器中还有construct和apply,它们是应用于函数对象的:
Reflect使用详解 Reflect的作用
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
那么这个Reflect有什么用呢?
它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
另外在使用Proxy时,可以做到不操作原对象;
那么Object和Reflect对象之间的API关系,可以参考MDN文档:
Reflect的常见方法
Reflect的使用
那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:
Receiver的作用
我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?
如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this;
Reflect的construct
迭代器与生成器详解 什么是迭代器
迭代器(iterator),使用户在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。
其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。
在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):
迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;
在JavaScript中这个标准就是一个特定的next方法;
next方法有如下的要求:
一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
done(boolean)
如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
value
迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
可迭代对象
但是上面的代码整体来说看起来是有点奇怪的:
我们获取一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;
事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;
什么又是可迭代对象呢?
它和迭代器是不同的概念;
当一个对象实现了iterable protocol协议时,它就是一个可迭代对象;
这个对象的要求是必须实现 @@iterator 方法,在代码中我们使用 Symbol.iterator 访问该属性;
当然我们要问一个问题,我们转成这样的一个东西有什么好处呢?
当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作;
比如 for…of 操作时,其实就会调用它的 @@iterator 方法;
原生迭代器对象
事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:
String、Array、Map、Set、arguments对象、NodeList集合;
可迭代对象的应用
那么这些东西可以被用在哪里呢?
JavaScript中语法:for …of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment);
创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]);
一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);
自定义类的迭代
迭代器的中断
迭代器在某些情况下会在没有完全迭代的情况下中断:
比如遍历的过程中通过break、return、throw中断了循环操作;
比如在解构的时候,没有解构所有的值;
那么这个时候我们想要监听中断的话,可以添加return方法:
什么是生成器
生成器函数执行
我们发现下面的生成器函数foo的执行体压根没有执行,它只是返回了一个生成器对象。
那么我们如何可以让它执行函数中的东西呢?调用next即可;
我们之前学习迭代器时,知道迭代器的next是会有返回值的;
但是我们很多时候不希望next返回的是一个undefined,这个时候我们可以通过yield来返回结果;
next函数
函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
答案是可以的;
我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值;
注意:也就是说我们是为本次的函数代码块执行提供了一个值;
return函数
还有一个可以给生成器函数传递参数的方法是通过return函数:
return传值后这个生成器函数就会结束,之后调用next不会继续生成值了;
throw函数
除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常:
抛出异常后我们可以在生成器函数中捕获异常;
但是在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行;
生成器替代迭代器
我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
事实上我们还可以使用yield*来生产一个可迭代对象:
这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值;
生成器实现
既然生成器是一个迭代器,那么我们可以对其进行如下的操作:
异步处理方案
学完了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。
案例需求:
我们需要向服务器发送网络请求获取数据,一共需要发送三次请求;
第二次的请求url依赖于第一次的结果;
第三次的请求url依赖于第二次的结果;
依次类推;
Generator方案
但是上面的代码其实看起来也是阅读性比较差的,有没有办法可以继续来对上面的代码进行优化呢?
目前我们的写法有两个问题:
第一,我们不能确定到底需要调用几层的Promise关系;
第二,如果还有其他需要这样执行的函数,我们应该如何操作呢?
所以,我们可以封装一个工具函数execGenerator自动执行生成器函数:
async/await 异步函数 async function
async关键字用于声明一个异步函数:
async是asynchronous单词的缩写,异步、非同步;
sync是synchronous单词的缩写,同步、同时;
async异步函数可以有很多中写法:
异步函数的执行流程
await关键字
async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的。
await关键字有什么特点呢?
通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise;
那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数;
如果await后面是一个普通的值,那么会直接返回这个值;
如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;
如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的reject值;
进程和线程 进程和线程
线程和进程是操作系统中的两个概念:
进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式;
线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中;
听起来很抽象,这里还是给出我的解释:
进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程);
线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程;
所以我们也可以说进程是线程的容器;
再用一个形象的例子解释:
操作系统类似于一个大工厂;
工厂中里有很多车间,这个车间就是进程;
每个车间可能有一个以上的工人在工厂,这个工人就是线程;
操作系统的工作方式
操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?
这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换;
当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码;
对于用户来说是感受不到这种快速的切换的;
你可以在Mac的活动监视器或者Windows的资源管理器中查看到很多进程:
浏览器中的JS线程
我们经常会说JavaScript是单线程(可以开启workers)的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node。
浏览器是一个进程吗,它里面只有一个线程吗?
目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;
每个进程中又有很多的线程,其中包括执行JavaScript代码的线程;
JavaScript的代码执行是在一个单独的线程中执行的:
这就意味着JavaScript的代码,在同一个时刻只能做一件事;
如果这件事是非常耗时的,就意味着当前的线程就会被阻塞;
所以真正耗时的操作,实际上并不是由JavaScript线程在执行的:
浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作;
比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可;
浏览器的事件循环 浏览器的事件循环
如果在执行JavaScript代码的过程中,有异步操作呢?
中间我们插入了一个setTimeout的函数调用;
这个函数被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行;
宏任务和微任务
但是事件循环中并非只维护着一个队列,事实上是有两个队列:
宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等
那么事件循环对于两个队列的优先级是怎么样的呢?
1.main script中的代码优先执行(编写的顶层script代码);
2.在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
也就是宏任务执行之前,必须保证微任务队列是空的;
如果不为空,那么就优先执行微任务队列中的任务(回调);
常见面试题
Node的事件循环 Node的事件循环
浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。
这里我们来给出一个Node的架构图:
我们会发现libuv中主要维护了一个EventLoop和worker threads(线程池);
EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等
libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方;
Node事件循环的阶段
我们最前面就强调过,事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:
无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中;
事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;
但是一次完整的事件循环Tick分成很多个阶段:
定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED。
idle, prepare:仅系统内部使用。
轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调;
检测(check):setImmediate() 回调函数在这里执行。
关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)。
宏任务和微任务
我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:
宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;
但是,Node中的事件循环不只是 微任务队列和 宏任务队列:
微任务队列:
next tick queue:process.nextTick;
other queue:Promise的then回调、queueMicrotask;
宏任务队列:
timer queue:setTimeout、setInterval;
poll queue:IO事件;
check queue:setImmediate;
close queue:close事件;
Node事件循环顺序
所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:
next tick microtask queue;
other microtask queue;
timer queue;
poll queue;
check queue;
close queue;
常见面试题
错误处理方案 错误处理方案
开发中我们会封装一些工具函数,封装之后给别人使用:
在其他人使用的过程中,可能会传递一些参数;
对于函数来说,需要对这些参数进行验证,否则可能得到的是我们不想要的结果;
很多时候我们可能验证到不是希望得到的参数时,就会直接return:
但是return存在很大的弊端:调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined;
事实上,正确的做法应该是如果没有通过某些验证,那么应该让外界知道函数内部报错了;
如何可以让一个函数告知外界自己内部出现了错误呢?
throw语句:
throw语句用于抛出一个用户自定义的异常;
当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行);
如果我们执行代码,就会报错,拿到错误信息的时候我们可以及时的去修正代码。
throw关键字
throw表达式就是在throw后面可以跟上一个表达式来表示具体的异常信息:
throw关键字可以跟上哪些类型呢?
基本数据类型:比如number、string、Boolean
对象类型:对象类型可以包含更多的信息
但是每次写这么长的对象又有点麻烦,所以我们可以创建一个类
Error类型
事实上,JavaScript已经给我们提供了一个Error类,我们可以直接创建这个类的对象:
Error包含三个属性:
messsage:创建Error对象时传入的message;
name:Error的名称,通常和类的名称一致;
stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;
Error有一些自己的子类:
RangeError:下标值越界时使用的错误类型;
SyntaxError:解析语法错误时使用的错误类型;
TypeError:出现类型错误时,使用的错误类型;
异常的处理
我们会发现在之前的代码中,一个函数抛出了异常,调用它的时候程序会被强制终止:
这是因为如果我们在调用一个函数时,这个函数抛出了异常,但是我们并没有对这个异常进行处理,那么这个异常会继续传递到上一个函数调用中;
而如果到了最顶层(全局)的代码中依然没有对这个异常的处理代码,这个时候就会报错并且终止程序的运行;
我们先来看一下这段代码的异常传递过程:
foo函数在被执行时会抛出异常,也就是我们的bar函数会拿到这个异常;
但是bar函数并没有对这个异常进行处理,那么这个异常就会被继续传递到调用bar函数的函数,也就是test函数;
但是test函数依然没有处理,就会继续传递到我们的全局代码逻辑中;
依然没有被处理,这个时候程序会终止执行,后续代码都不会再执行了;
异常的捕获
但是很多情况下当出现异常时,我们并不希望程序直接推出,而是希望可以正确的处理异常:
这个时候我们就可以使用try catch
在ES10(ES2019)中,catch后面绑定的error可以省略。
当然,如果有一些必须要执行的代码,我们可以使用finally来执行:
finally表示最终一定会被执行的代码结构;
注意:如果try和finally中都有返回值,那么会使用finally当中的返回值;
本地存储Storage 认识Storage
WebStorage主要提供了一种机制,可以让浏览器提供一种比cookie更直观的key、value存储方式:
localStorage:本地存储,提供的是一种永久性的存储方法,在关闭掉网页重新打开时,存储的内容依然保留;
sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除;
我们会发现localStorage和sessionStorage看起来非常的相似。
那么它们有什么区别呢?
验证一:关闭网页后重新打开,localStorage会保留,而sessionStorage会被删除;
验证二:在页面内实现跳转,localStorage会保留,sessionStorage也会保留;
验证三:在页面外实现跳转(打开新的网页),localStorage会保留,sessionStorage不会被保留;
Storage常见方法和属性
Storage有如下的属性和方法:
属性:
Storage.length:只读属性
返回一个整数,表示存储在Storage对象中的数据项数量;
方法:
Storage.key(index):该方法接受一个数值n作为参数,返回存储中的第n个key名称;
Storage.getItem():该方法接受一个key作为参数,并且返回key对应的value;
Storage.setItem():该方法接受一个key和value,并且将会把key和value添加到存储中。
Storage.removeItem():该方法接受一个key作为参数,并把该key从存储中删除;
Storage.clear():该方法的作用是清空存储中的所有key;
正则表达式(空) 防抖与节流 认识防抖和节流函数
防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;
防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题。
但是很多前端开发者面对这两个功能,有点摸不着头脑:
某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);
某些开发者可以区分,但是不知道如何应用;
某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写;
接下来我们会一起来学习防抖和节流函数:
我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;
并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;
认识防抖debounce函数
我们用一副图来理解一下它的过程:
当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
当事件密集触发时,函数的触发会被频繁的推迟;
只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
防抖的应用场景很多:
输入框中频繁的输入内容,搜索或者提交信息;
频繁的点击按钮,触发某个事件;
监听浏览器滚动事件,完成某些特定操作;
用户缩放浏览器的resize事件;
防抖函数的案例
我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容:
比如想要搜索一个MacBook:
当我输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;
当继续输入ma时,再次发送网络请求;
那么macbook一共需要发送7次网络请求;
这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;
但是我们需要这么多次的网络请求吗?
不需要,正确的做法应该是在合适的情况下再发送网络请求;
比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;
比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;
也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;
这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数;
案例准备
我们通过一个搜索框来延迟防抖函数的实现过程:
测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作:
Underscore库的介绍
事实上我们可以通过一些第三方库来实现防抖操作:
这里使用underscore
我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;
但是目前我看到underscore还在维护,lodash已经很久没有更新了;
Underscore的官网: https://underscorejs.org/
Underscore的安装有很多种方式:
下载Underscore,本地引入;
通过CDN直接引入;
通过包管理工具(npm)管理安装;
这里我们直接通过CDN:
1 <scriptsrc="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script >
认识节流throttle函数
我们用一副图来理解一下节流的过程
当事件触发时,会执行这个事件的响应函数;
如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;
节流的应用场景:
监听页面的滚动事件;
鼠标移动事件;
用户频繁点击按钮操作;游戏中的一些设计;
节流函数的应用场景
很多人都玩过类似于飞机大战的游戏
在飞机大战的游戏中,我们按下空格会发射一个子弹:
很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;
比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射;
但是事件是触发了10次的,响应的函数只触发了一次;
生活中防抖的例子:
比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。
如果在五分钟的时间内,没有同学问我问题,那么我就下课了;
在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;
如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);
生活中节流的例子:
比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题;
如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;
自定义防抖节流函数
我们按照如下思路来实现:
防抖基本功能实现:可以实现防抖效果
优化一:优化参数和this指向
优化二:优化取消操作(增加取消功能)
优化三:优化立即执行效果(第一次立即执行)
优化四:优化返回值
我们按照如下思路来实现:
节流函数的基本实现:可以实现节流效果
优化一:节流最后一次也可以执行
优化二:优化添加取消功能
优化三:优化返回值问题
自定义深拷贝函数
前面我们已经学习了对象相互赋值的一些关系,分别包括:
引入的赋值:指向同一个对象,相互之间会影响;
对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
对象的深拷贝:两个对象不再有任何关系,不会相互影响;
前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse
这种深拷贝的方式其实对于函数、Symbol等是无法处理的;
并且如果存在对象的循环引用,也会报错的;
自定义深拷贝函数:
1.自定义深拷贝的基本功能;
2.对Symbol的key进行处理;
3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;
4.对循环引用的处理;
自定义事件总线
自定义事件总线属于一种观察者模式,其中包括三个角色:
发布者(Publisher):发出事件(Event);
订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);
事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;
当然我们可以选择一些第三方的库:
Vue2默认是带有事件总线的功能;
Vue3中推荐一些第三方库,比如mitt;
当然我们也可以实现自己的事件总线:
事件的监听方法on;
事件的发射方法emit;
事件的取消监听off;