You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functioncreate(){letobj={}letCon=[].shift.call(arguments)obj.__proto__=Con.prototypeletresult=Con.apply(obj,arguments)returnresultinstanceofObject ? result : obj}
classAnimal{constructor(legsNumber){this.legsNumber=legsNumber}run(){}}classDogextendsAnimal{constructor(name){super(4)this.name=name}say(){console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)}}constd2=newDog('旺财')// Dog 函数就是一个类console.dir(d2)
vara={name: 'A',fn: function(){console.log(this.name)}}a.fn()// this === aa.fn.call({name: 'B'})// this === {name: 'B'}varfn1=a.fnfn1()// this === window
用 var 声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。let 和 const 是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中访问。
var 声明的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
functionfoo(){// 所有变量在函数中都可访问varbar='bar'letbaz='baz'constqux='qux'console.log(bar)// barconsole.log(baz)// bazconsole.log(qux)// qux}console.log(bar)// ReferenceError: bar is not definedconsole.log(baz)// ReferenceError: baz is not definedconsole.log(qux)// ReferenceError: qux is not defined
if(true){varbar='bar'letbaz='baz'constqux='qux'}// 用 var 声明的变量在函数作用域上都可访问console.log(bar)// bar// let 和 const 定义的变量在它们被定义的语句块之外不可访问console.log(baz)// ReferenceError: baz is not definedconsole.log(qux)// ReferenceError: qux is not defined
var 会使变量提升,这意味着变量可以在声明之前使用。let 和 const 不会使变量提升,并且在变量未申明前不可使用(即暂时性死区)。
console.log(foo)// undefinedvarfoo='foo'console.log(baz)// ReferenceError: can't access lexical declaration 'baz' before initializationletbaz='baz'console.log(bar)// ReferenceError: can't access lexical declaration 'bar' before initializationconstbar='bar'
用 var 重复声明不会报错,但 let 和 const 会。
varfoo='foo'varfoo='bar'console.log(foo)// "bar"letbaz='baz'letbaz='qux'// Uncaught SyntaxError: Identifier 'baz' has already been declared
let 和 const 的区别在于:let 声明的变量可以任意修改,const 声明为值类型数据时不可变,声明值为引用类型的时候只允许修改内存中存储的值,不允许直接修改指针。
前言
Javascript 基础知识
数据类型
JavaScript 中的值都具有特定的类型。例如,字符串或数字。
在 JavaScript 中有 8 种基本的数据类型(7 种原始类型和 1 种引用类型)。
我们可以将任何类型的值存入变量。例如,一个变量可以在前一刻是个字符串,下一刻就存储一个数字:
允许这种操作的编程语言,例如 JavaScript,被称为“动态类型”(dynamically typed)的编程语言,意思是虽然编程语言中有不同的数据类型,但是你定义的变量并不会在定义后,被限制为某一数据类型。
原始类型
在 JS 中,存在着 7 种原始值,分别是:
其中 Symbol 和 BigInt 是 ES6 中新增的数据类型:
首先原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString();
此时你肯定会有疑问,这不对呀,明明 '1'.toString() 是可以使用的。其实在这种情况下,'1' 已经不是原始类型了,而是被强制转换成了 String 类型也就是对象类型,所以可以调用 toString 函数。
除了会在必要的情况下强转类型以外,原始类型还有一些坑。
string 类型是不可变的,无论你在 string 类型上调用何种方法,都不会对值有改变。
相比较于其他编程语言,JavaScript 中的 null 不是一个“对不存在的 object 的引用”或者 “null 指针”。
JavaScript 中的 null 仅仅是一个代表“无”、“空”或“值未知”的特殊值。用 null 声明的变量表示其是未知的。
对象类型
在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是
值
,对象类型存储的是地址
(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。对于常量 a 来说,假设内存地址(指针)为 #1,那么在地址 #1 的位置存放了值 [],常量 a 存放了地址(指针) #1,再看以下代码
当我们将变量赋值给另外一个变量时,复制的是原本变量的地址(指针),也就是说当前变量 b 存放的地址(指针)也是 #1,当我们进行数据修改的时候,就会修改存放在地址(指针) #1 上的值,也就导致了两个变量的值都发生了改变。
两种类型的区别
两种类型的区别在于存储位置的不同:
堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
在操作系统中,内存被分为栈区和堆区:
null 和 undefined 的区别
null
表示一个对象是“没有值”的值,也就是值为“空”undefined
表示一个变量声明了没有初始化(赋值)undefined
和null
在 if 语句中,都会被自动转为 falseundefined
不是一个有效的 JSON,而null
是undefined
的类型(typeof)是undefined
null
的类型(typeof)是object
Javascript 将未赋值的变量默认值设为
undefined
Javascript 从来不会将变量设为
null
。 它是用来表明某个声明的变量是没有值、未知的值类型 vs 引用类型
ES 分为原始类型和引用类型,只有
object
和function
是引用类型,其他都是值类型。根据 JavaScript 中的变量类型传递方式,又分为值类型和引用类型,值类型变量包括 Boolean、String、Number、Undefined、Null,引用类型包括了 Object 类的所有,如 Date、Array、Function 等。在参数传递方式上,值类型是按值传递,引用类型是按共享传递。
下面通过一个小题目,来看下两者的主要区别,以及实际开发中需要注意的地方。
上述代码中,
a
b
都是值类型,两者分别修改赋值,相互之间没有任何影响。再看引用类型的例子:上述代码中,
a
b
都是引用类型。在执行了b = a
之后,修改b
的属性值,a
的也跟着变化。因为a
和b
都是引用类型,指向了同一个内存地址,即两者引用的是同一个值,因此b
修改属性时,a
的值随之改动。再借助题目进一步讲解一下。
通过代码执行,会发现:
a
的值没有发生改变b
的值发生了改变这就是因为
Number
类型的a
是按值传递的,而Object
类型的b
是按共享传递的。JS 中这种设计的原因是:按值传递的类型,复制一份存入栈内存,这类类型一般不占用太多内存,而且按值传递保证了其访问速度。按共享传递的类型,是复制其引用,而不是整个复制其值(C 语言中的指针),保证过大的对象等不会因为不停复制内容而造成内存的浪费。
引用类型经常会在代码中按照下面的写法使用,或者说容易不知不觉中造成错误!
虽然
obj
本身是个引用类型的变量(对象),但是内部的a
和b
一个是值类型一个是引用类型,a
的赋值不会改变obj.a
,但是b
的操作却会反映到obj
对象上。内置函数(原生函数)
原始值 "I am a string" 并不是一个对象,它只是一个字面量,并且是一个不可变的值。
如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为 String 对象。
幸好,在必要时语言会自动把字符串字面量转换成一个 String 对象,也就是说你并不需要显式创建一个对象。
类型判断
typeof vs instanceof
typeof 对于原始类型来说,除了 null 都可以显示正确的类型,null 可以使用
String(null) === 'null'
来判断typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型
如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的。instanceof 能在实例的 原型对象链 中找到该构造函数的 prototype 属性所指向的 原型对象,就返回 true。即:
Object.prototype.toString.call(obj)
很稳的类型判断, 调用 toString 后根据[object XXX]进行判断
类型转换
隐式转换
在 JS 中在使用运算符号或者对比符时,会自带隐式转换:
转 Boolean
在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,其他所有值都转为 true,包括所有对象。
转字符串
转数字
对象的拷贝
浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响
Object.assign
深拷贝: 完全拷贝一个新对象,修改时原对象不再受到任何影响
JSON.parse(JSON.stringify(obj))
: 性能最快undefined
、或symbol
时,无法拷贝这种方法有缺陷,详情请看关于 JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑
基础版(新增函数函数类型支持),推荐使用 lodash 的深拷贝函数。
参考链接:
数组(array) API
改变原数组
unshift / shift
: 头部推入和弹出,改变原数组,unshift
返回数组长度,shift
返回原数组第一项 ;push / pop
: 末尾推入和弹出,改变原数组,push
返回数组长度,pop
返回原数组最后一项;sort(fn) / reverse
: 排序与反转,改变原数组splice(start, number, value...)
: 返回删除元素组成的数组,value 为插入项,改变原数组不改变原数组
map
: 遍历数组,返回回调返回值组成的新数组forEach
: 无法break
,可以用try/catch
中throw new Error
来停止filter
: 过滤some
: 有一项返回true
,则整体为true
every
: 有一项返回false
,则整体为false
join
: 通过指定连接符生成字符串concat
: 连接数组,不影响原数组, 浅拷贝slice(start, end)
: 返回截断后的新数组,不改变原数组indexOf / lastIndexOf(value, fromIndex)
: 查找数组项,返回对应的下标reduce / reduceRight(fn(prev, cur), defaultPrev)
: 两两执行,prev 为上次化简函数的return
值,cur 为当前值defaultPrev
时,从第一项开始;参考链接
如何判断数组与对象
数组去重
ES5
ES6
空间换时间
原型 / 构造函数 / 实例
原型
(prototype)
: 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript
对象中都包含一个__proto__
(非标准)的属性指向它爹(该对象的原型),可obj.__proto__
进行访问。构造函数: 可以通过
new
来 新建一个对象 的函数。实例: 通过构造函数和
new
创建出来的对象,便是实例。 实例通过__proto__
指向原型,通过constructor
指向构造函数。说了一大堆,大家可能有点懵逼,这里来举个栗子,以
Object
为例,我们常用的Object
便是一个构造函数,因此我们可以通过它构建实例。则此时, 实例为
instance
, 构造函数为Object
,我们知道,构造函数拥有一个prototype
的属性指向原型,因此原型为:这里我们可以来看出三者的关系:
参考链接
原型链
原型链是由原型对象组成,每个对象都有
__proto__
属性,指向了创建该对象的构造函数的原型,__proto__
将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象
Object.prototype
,如还是没找到,则输出undefined
;属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用:
b.prototype.x = 2
;但是这样会造成所有继承于该对象的实例的属性发生改变。instanceof 原理
语法:object instanceof constructor
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
能在实例的 原型对象链 中找到该构造函数的
prototype
属性所指向的原型对象
,就返回 true。即:参考链接
new 操作符的执行过程
2.
设置新对象的 constructor 属性为构造函数的名称(从原型链上取),设置新对象的****proto****属性指向构造函数的 prototype 对象手写一个
bind、call、apply 的区别
call 和 apply 其实是一样的,区别就在于传参时参数是一个一个传或者是以一个数组的方式来传。
call 和 apply 都是在调用时生效,改变调用者的 this 指向。
bind 也是改变 this 指向,不过不是在调用时生效,而是返回一个新函数。
实现 bind call apply 函数
bind
call
apply
JS 如何实现继承?
方法一:使用原型链
被 ban 的代码替换方式
方法二:使用 class
浮点数精度
因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。
对于这个问题,还有一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对 JavaScript 来说,这个值通常为 2-52,在 ES6 中,提供了 Number.EPSILON 属性,而它的值就是 2-52,只要判断
0.1+0.2-0.3
是否小于Number.EPSILON
,如果小于,就可以判断为 0.1+0.2 ===0.3变量提升
var 会使变量提升,这意味着变量可以在声明之前使用。let 和 const 不会使变量提升,提前使用会报错。
变量提升(hoisting)是用于解释代码中变量声明行为的术语。使用 var 关键字声明或初始化的变量,会将声明语句“提升”到当前作用域的顶部。 但是,只有声明才会触发提升,赋值语句(如果有的话)将保持原样。
函数提升,需要注意函数声明和函数表达式的区别。无论在哪儿定义函数,只要是外层函数并且满足不被包裹,就都可以进行全局范围的调用。而函数表达式需要等到表达式赋值 完成 才可以使用。
作用域
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
执行上下文
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
对于每个执行上下文,都有三个重要属性:
全局上下文
在全局上下文中的变量对象就是全局对象。
函数上下文
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
函数上下文的变量对象初始化只包括 Arguments 对象
在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
在代码执行阶段,会再次修改变量对象的属性值
作用域链
我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数,原理和原型链很相似。
由两部分组成:
如此
[[scope]]
包含[[scope]]
,便自上而下形成一条链式作用域。一个例子
以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:
执行过程如下:
1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
5.第三步:将活动对象压入 checkscope 作用域链顶端
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
思考闭包的作用域链:
参考链接
闭包
闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的
[[scope]]
中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。使⽤闭包主要是为了设计私有的⽅法和变量。
闭包有三个特性:
闭包的优点是简单好用避免全局变量的污染的同时提供对局部变量的访问,缺点是闭包会常驻内存,会增⼤内存使⽤量,使⽤不当很容易造成内存泄露。
闭包会产生一个很经典的问题(在循环中使用闭包):
多个子函数的
[[scope]]
都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。解决:
函数参数
的形式 传入,避免使用默认的[[scope]]向上查找setTimeout
包裹,通过第三个参数传入块级作用域
,让变量成为自己上下文的属性,避免共享例子 1:
犯错原因是在循环的过程中,并没有把函数的返回值赋值给数组元素,而仅仅是把函数赋值给了数组元素。这就使得在调用匿名函数时,通过作用域找到的执行环境中 AO 中查找不到 i 而再往函数外的 VO 查找,这时,变量的值已经不是循环时的瞬时索引值,而是循环执行完毕之后的索引值。
ar[0]()
访问 bar 的第 0 个元素并执行。此时,执行栈创建并进入匿名函数执行环境,匿名函数中存在自由变量 i,需要使用其作用域链匿名函数 -> foo()函数 -> 全局作用域进行查找,最终在 foo()函数的作用域找到了 i,然后在 foo()函数的执行环境中找到了 i 的值 2,于是给 i 赋值 2.解决方案 1:IIFE
由此,可以利用 IIFE 传参和闭包来创建多个执行环境来保存循环时各个状态的索引值。因为函数传参是按值传递的,不同参数的函数被调用时,会创建不同的执行环境
解决方案 2:块作用域
使用 IIFE 还是较为复杂,使用块作用域则更为方便
由于块作用域可以将索引值 i 重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值,相当于为每一次索引值都创建一个执行环境
参考: https://www.xiaohuochai.site/JS/ECMA/closure/commonError.html
例子 2: 不正确的无法打印索引:
改造为 5 -> 0,1,2,3,4
改造为 0 -> 1 -> 2 -> 3 -> 4 -> 5
原有的代码块中的循环和两处 console.log 不变,该怎么改造代码?新的需求可以精确的描述为:代码执行时,立即输出 0,之后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5。
参考链接
事件触发的流程
事件委托
事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。
参考链接
事件模型
DOM0
直接绑定
DOM2
DOM3
参考资料:
如何自定义事件
事件列表
事件参考-MDN
新模式
过时的模式
document.createEvent('Event')
创建事件initEvent
初始化事件dispatchEvent
触发事件this 的指向
先搞明白一个很重要的概念 ——
this
的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this
是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子a.fn()
)fn1()
)a.fn.call({name: 'B'})
)参考链接
事件循环机制 Event-Loop
众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。
JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
以上代码虽然 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimeout 还是会在 script end 之后打印。
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。
属于微任务(microtask)的事件有以下几种:
属于宏任务(macrotask)的事件有以下几种:
很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。
所以正确的一次 Event loop 顺序是这样的
通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。
参考链接
js 脚本加载,阻塞与异步
JS 脚本资源的处理有几个特点:
注意,defer 和 async 是有区别的: defer 是延迟执行,而 async 是异步执行。
简单的说:
async
是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在onload
前,但不确定在DOMContentLoaded
事件的前或后defer
是延迟执行,在浏览器看起来的效果像是将脚本放在了body
后面一样(虽然按规范应该是在DOMContentLoaded
事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)节流与防抖
防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。
函数柯里化
在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。
自执行函数?用于什么场景?好处?
例子
自执行函数:
作用:创建一个独立的作用域。
好处
场景
一般用于框架、插件等场景
arguments 对象了解吗?如何转换为数组?
arguments 是一个对应于传递给函数的参数的类数组对象。arguments 对象是所有(非箭头)函数中都可用的局部变量。
参考链接
使用 let、var 和 const 创建变量有什么区别
用 var 声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。let 和 const 是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中访问。
var 声明的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
var 会使变量提升,这意味着变量可以在声明之前使用。let 和 const 不会使变量提升,并且在变量未申明前不可使用(即暂时性死区)。
用 var 重复声明不会报错,但 let 和 const 会。
let 和 const 的区别在于:let 声明的变量可以任意修改,const 声明为值类型数据时不可变,声明值为引用类型的时候只允许修改内存中存储的值,不允许直接修改指针。
箭头函数和普通函数有什么区别
ES6 允许使用“箭头”(=>)定义函数。
function name(arg1, arg2) {...}
可以使用(arg1, arg2) => {...}
来定义。箭头函数的使用注意点:
箭头函数存在的意义,第一写起来更加简洁,第二可以解决 ES6 之前函数执行中
this
是全局变量的问题,看如下代码:Promise
Promise
是 CommonJS 提出来的这一种规范,有多个版本,在 ES6 当中已经纳入规范,原生支持 Promise 对象,非 ES6 环境可以用类似 Bluebird、Q 这类库来支持。Promise
可以将回调变成链式调用写法,流程更加清晰,代码更加优雅。简单归纳下 Promise:三个状态、两个过程、一个方法,快速记忆方法:3-2-1
三个状态:
pending
、fulfilled
、rejected
两个过程:
一个方法:
then
当然还有其他概念,如
catch
、Promise.all/race/allSettled
,这里就不展开了。Async & Await
async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。
函数前面的关键字 async 有两个作用:
Promise 前的关键字 await 使 JavaScript 引擎等待该 promise settle,然后:
这两个关键字一起提供了一个很好的用来编写异步代码的框架,这种代码易于阅读也易于编写。
有了 async/await 之后,我们就几乎不需要使用 promise.then/catch,但是不要忘了它们是基于 promise 的,因为有些时候(例如在最外层作用域)我们不得不使用这些方法。并且,当我们需要同时等待需要任务时,Promise.all 是很好用的。
Set 和 Map
Set 和 Map 都是 ES6 中新增的数据结构,是对当前 JS 数组和对象这两种重要数据结构的扩展。由于是新增的数据结构,目前尚未被大规模使用,但是作为前端程序员,提前了解是必须做到的。先总结一下两者最关键的地方:
Set
Set 实例不允许元素有重复,可以通过以下示例证明。可以通过一个数组初始化一个 Set 实例,或者通过
add
添加元素,元素不能重复,重复的会被忽略。Set 实例的属性和方法有
size
:获取元素数量。add(value)
:添加元素,返回 Set 实例本身。delete(value)
:删除元素,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是否是 Set 实例的元素。clear()
:清除所有元素,没有返回值。Set 实例的遍历,可使用如下方法
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。不过由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys()
和values()
返回结果一致。entries()
:返回键值对的遍历器。forEach()
:使用回调函数遍历每个成员。Map
Map 的用法和普通对象基本一致,先看一下它能用非字符串或者数字作为 key 的特性。
需要使用
new Map()
初始化一个实例,下面代码中set
get
has
delete
顾名即可思义(下文也会演示)。其中,map.set(obj, 'OK')
就是用对象作为的 key (不光可以是对象,任何数据类型都可以),并且后面通过map.get(obj)
正确获取了。Map 实例的属性和方法如下:
size
:获取成员的数量set
:设置成员 key 和 valueget
:获取成员属性值has
:判断成员是否存在delete
:删除成员clear
:清空所有Map 实例的遍历方法有:
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回所有成员的遍历器。forEach()
:遍历 Map 的所有成员。Class
class 其实一直是 JS 的关键字(保留字),但是一直没有正式使用,直到 ES6 。 ES6 的 class 就是取代之前构造函数初始化对象的形式,从语法上更加符合面向对象的写法。例如:
JS 构造函数的写法
用 ES6 class 的写法
注意以下几点,全都是关于 class 语法的:
class Name {...}
这种形式,和函数的写法完全不一样constructor
函数中,constructor
即构造器,初始化实例时默认执行add() {...}
这种形式,并没有function
关键字使用 class 来实现继承就更加简单了,至少比构造函数实现继承简单很多。看下面例子
JS 构造函数实现继承
ES6 class 实现继承
注意以下两点:
extends
即可实现继承,更加符合经典面向对象语言的写法,如 Javaconstructor
一定要执行super()
,以调用父类的constructor
Object 与 Map 的区别
选择 Object 还是 Map
对于多数 Web 开发任务来说,选择 Object 还是 Map 只是个人偏好问题,影响不大。不过,对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别。
1. 内存占用
Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。
不同浏览器的情况不同,但给定固定大小的内存, Map 大约可以比 Object 多存储 50%的键/值对。
2. 插入性能
向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。
如果代码涉及大量插入操作,那么显然 Map 的性能更佳。
3. 查找速度
与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。
这对 Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。
4. 删除性能
使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null 。但很多时候,这都是一
种讨厌的或不适宜的折中。
而对大多数浏览器引擎来说, Map 的 delete() 操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择 Map 。
参考资料:
代码的复用
当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:
模块化
模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中使用 commonjs 的模块化支持。
浏览器的垃圾回收机制
(1)垃圾回收的概念
垃圾回收:JavaScript 代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
(2)垃圾回收的方式
浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。
1)标记清除
2)引用计数
obj1
和obj2
通过属性进行相互引用,两个对象的引用次数都是 2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1
和obj2
还将会继续存在,因此它们的引用次数永远不会是 0,就会引起循环引用。这种情况下,就要手动释放变量占用的内存:
(3)减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
object
进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为 null,尽快被回收。参考
The text was updated successfully, but these errors were encountered: