9月抽空重新回顾了下ES6所有知识点,整个回顾过程既惊喜又感慨,感慨开发这么久好像真的没有好好的静下心去读一本好的书,大多情况下只是在使用的时候用到了,不熟悉或者感兴趣再去走马观花一通,感慨之余也发现了一些自身的的问题,知识体系还是不够丰富、扎实,一句话:有空多读书总没错,好了不闲扯了,下面我们步入正题
关于JavaScript
如今前端知识日新月异,越是晚入门小伙伴,很多基础层面的东西,接触的真的是少之又少,各种前端框架层粗不穷Vue
、React
、Ng
这三大带有里程碑意义的框架Api
我想看的这么文字的盆友应该都有接触,基础的Api
使用应该是没有什么难度,但是在遇到问题,解决问题、以及在原有组件以及自我封装组件,搭建框架过程总会遇到一些莫名其妙的问题,解决这个问题,看源码
是大众都知道的一些方法,但个人认为这是一种进阶的方法,新手入门建议可以先了解咱们JavaScript的发展史
可以比较轻松的认识我们整个技术体系的发展和未来方向,避免一些认知错误
发展史
-
互联网早期
- BS架构:1990年的12月25日,西方的圣诞节,Tim Berners-Lee在他的NeXT电脑上部署了第一套
“主机-网站-浏览器”
构成的Web系统,这标志BS架构
的网站应用软件的开端,也是前端工程的开端。这个时候前端只是简单的Html静态文本
简单到基本动画都没有 - 1995年
javascript
的诞生:1995年,NetScape(网景)公司的工程师Brendan Eich设计了javascript脚本语言,并集成到了navigator2.0版本中。随后微软也意识到了javascript的潜力,并模仿开发VBScript和JScript应用到了IE中,这直接开启了NetScape和微软的浏览器竞争
。由于微软的IE集成在windows操作系统上的优势,NetScape的navigator很快在浏览器市场上落于下风。于是他们把javascript提交到了ECMA
,推动制订了ECMAScript标准
,成功实现了javascript的标准国际化。虽然第一次浏览器战争最后IE大胜Navigator,但是NetScape的javascript主导了W3C的官方标准。
- BS架构:1990年的12月25日,西方的圣诞节,Tim Berners-Lee在他的NeXT电脑上部署了第一套
-
互联网发展期
- 动态页面伊始:由于
javascript推动
,前端页面开始走向动态化,那时流行的跑马灯、悬浮广告
基本是各大门户网站的标配,页面基本都是通过PHP、JSP、ASP
动态渲染出来,这也直接导致后端代码的逻辑臃肿,服务端为了更好的管理代码,应运而生MVC模式
- Ajax出现:前期的动态性完全由服务端推动,每一次的动态更新都需要将页面进行
reload
操作,不管交互还是性能,都是不值当的 "这个问题直到谷歌在04年应用Ajax技术开发的Gmail和谷歌地图的发布,才得到了解决。" 这背后的秘密就是Ajax技术中实现的异步HTTP请求
,这让页面无需刷新就可以发起HTTP请求,用户也不用专门等待请求的响应,而是可以继续网页的浏览或操作。Ajax开启了web2.0的时代
- 前端兼容问题:由于更重浏览器差异性,兼容问题也是层出不穷,不同的浏览器技术标准有不小的差异,不利于兼容开发,这催生了
Dojo、Mooltools、YUIExtJS、jQuery
等前端兼容框架,其中jQuery应用最为广泛。这些框架中不知道大家用过几个 - Html5诞生:部分浏览器厂商为了解决适配问题提出过
Web Forms 2.0
、Web Applications 1.0
等规章最后整合成HTML5、各大浏览器都在为适配浏览器不断改善自己的浏览器 - Node.js:2009年,Ryan Dahl以Chrome的V8引擎为基础开发了基于
事件循环的异步I/O框架-Node.js
。Node.js使得前端开发人员可以利用javascript开发服务器端程序。很快,大量的Node.js使用者就建构了一个用NPM包管理工具管理的Node.js生态系统。node.js也能开发跨平台的桌面应用
Node 衍生出的
NPM
包管理工具为整个前端社区提供了一个规范良好的平台 - 动态页面伊始:由于
-
互联网进击
- 移动App、Hybrid App:移动互联网的到来,飞速发展,原生app迭代远远满足不了,大家找到折中的方法,损耗部分性能,提高产品产出;
jQuery Mobile、Sencha Touch、Framework7
如鱼得水;Hybrid技术指的是利用Web开发技术,调用Native相关的API,实现移动与Web二者的有机结合,既能利用Web开发周期短的优势,又能为用户提供Native的体验。 - ECMAScript6:2015年6月,
ECMAScript 6.0
发布,该版本增加了很多新的语法,极大的拓展了javascript的开发潜力;一些陈旧的浏览器可以通过Babel进行降级转义适配,ES6将JavaScript推向了另一个历史转折点 - 进行中:经过历史的沉淀,技术演进,交互升级,
React、Vue、Anjular
三大框架利用js集合自身优势,完全实现了目前的前后端分离的开发模式;开发体系发展到:NPM和Yarn为代表的包管理工具;ES6及Babel和TypeScript构成的脚本体系;HTML5;CSS3和相应的处理技术;React、Vue、Anjular为代表的框架;Webpack为代表的打包工具;Node.js为基础的Express和KOA后端框架;Hybrid技术。
- 移动App、Hybrid App:移动互联网的到来,飞速发展,原生app迭代远远满足不了,大家找到折中的方法,损耗部分性能,提高产品产出;
ECMAScript简介
提案规则
权重自上到下
- Stage 0 - Strawman(展示阶段)
- Stage 1 - Proposal(征求意见阶段)
- Stage 2 - Draft(草案阶段)
- Stage 3 - Candidate(候选人阶段)
- Stage 4 - Finished(定案阶段)
一般走到草案阶段基本可以在正式标准中看到,Tc39可查看各提案
Tips:
这也是在babel中配置presets
的来由(可能我们使用了一些仍然在草案甚至征求意见阶段的API)的时候需babel垫片
Babel转码器
配置.babelrc/babel.config.js
// 基本格式
{
"presets":[]
"plugins":[]
}
不同环境支持不同的转换方法
- 命令行 //@babel/cli
- 浏览器环境 //HTML中引入对应脚本
- node环境 //Traceur模块
一般我们使用的脚手架默认配置好,但是我们需要配置的什么意思,以及为什么要配置
@babel/register、babel-core/register
改写require
命令,为每个require引入的资源进行Babel转义
@babel/polyfill
Babel仅会转换新的语法,但是已有的Api中的Iterator、Generator、Set、Map、Proxy、Reflect、Symbol、Promise
是需要用这个垫片处理
@babel/core
暴露出Babel的Api,进行特殊操作
ES6基础篇
基础篇我们主要从es6中添加的一些命令,以及对已有数据类型拓展的汇总
let & const
let
特性如下
- 区别于
var
的块级作用域 - 不存在变量提升问题
- 具有暂时性死区
- 不能重复命名
// 下面这块很好的体现了`块级作用域`
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
特殊说明下暂时性死区的概念
,就是:在块中,let 声明变量之前都是不能使用的
const
const 和 let 特性基本一致,区别于,使用const定义的是显式的常量
,一经定义,不可更改
块级作用域
es5之前是有全局作用域、函数作用域
,es6推出的块级作用域但凡一个{}
就是一个作用域;
外层块级作用域能在内层使用,内层定义在外层访问不到
解构赋值
Tips:解构赋值
对应的还有拓展操作符
,解构不成功则为undefined;一看就懂,一用代码就简洁
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值(Iterator类型),这被称为解构(Destructuring)
数组的解构
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
对象的解构
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
数组对象的解构
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
函数参数的解构
// 设置默认值
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
注:
还有字符串解构(转换成数组)、字符/布尔解构(转换成对象),解构遵循的宗旨是模式、数据格式一致,即可解构,解构失败undefined;进行赋值的过程,不管是数组还是对象解构都可以设置默认值
;圆括号()
只能在赋值语句的非模式部分即可
用途优势
- 结构清晰,代码明了
-
函数中直接返回多个值
let [a,b,c] = fn()
- 函数入参清晰多参数
- JSON对象快速提取对应值
- 函数设置默认值
字符串
字符串中改动还是蛮多的有添加Unicode表示
、JSON.stringify()适配非UTF-8编码
,下面介绍日常开发实用改变
添加Iterator接口
for of
遍历器(es6新增)仅对拥有Iterator接口的适配
模板字符串
`` 反引号 节省我们字符串拼接的痛点
let str1 =
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ';
let str2 = `There arebasket.count ${variable}</b>items in your basket, `;
上面字符串拼接自上到下的升级,同时也可以使用${}
动态注入变量
模板编译
<%...%>
中放置JavaScript代码,如下
let template = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
可以利用字符串匹配<%...%>
封装template转换函数
字符串方法
- String.fromCodePoint()// 识别大于
0xFFFF
的字符 - String.raw() // 返回斜杠都转义的字符
-
codePointAt()
charAt() // UTF-8 codePointAt() // UTF-16
- normalize() //处理合成字符串
-
includes(), startsWith(), endsWith()
includes():返回布尔值,表示是否找到了参数字符串。 startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。 endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
- repeat() // 返回一个新字符,参数是重复次数
- padStart(),padEnd() // 头/尾部补全字符 两个参数第一个长度、第二个填补的str
- trimStart(),trimEnd() // 消除头/尾部空格
- matchAll() // 字符正则匹配 参数正则表达式
数值类型
二进制、八进制表示法
0b(0B)二进制;0o(0O)八进制
方法
- Number.isFinite(), Number.isNaN()
判断是否有限、判断参数类型是否为NAN - Number.parseInt(), Number.parseFloat()
将全局方法移植到内置对象下 - Number.isInteger() 判断是否是整数
- Number.EPSILON // 常量 表示 1 与大于 1 的最小浮点数之间的差。
- Math.trunc() // 返回整数
- Math.sign() // 判断是否正数、负数、零
- Math.cbrt() // 计算数组的立方根
- Math.clz32() // 返回 32位前置零个数
- Math.imul // 返回两个数以 32 位带符号整数形式相乘的结果
- ...
拓展符号(右结合即右边先运算
)
**
、**=
操作符 进行指数运算
//指数拓展符
let a = 2;
a ** 2 **4 // 2 * (2 * 4)
a **= 3 // a = 2 * 2 * 2
数组拓展
展开运算符
...
将一个数组转成用逗号分隔的参数排列
console.log(1,2 ...[3,4]) // 1,2,3,4
方法拓展
-
Array.form()
// 将类数组转换成数组 -
Array.of()
// 用于将一组值,转换为数组
实例方法拓展
- find()、findIndex() // find找到第一个满足条件的值,findIndex和前者类似,但是返回是下标,找不到返回
-1
- fill() 数组填充(params0:填充的值,params1:开始下坐标,params2:结束下坐标)
- entries(),keys() 和 values() // 返回数组的键值对
- includes() //是否包含某个值,
返回bool值
-
flat()、flatMap()
// 扯平数组[1, 2, , [4, 5]].flat() //[1, 2, 4, 5] 默认拉平一层,参数num、Infinity自定义拉平层数目 // flatMap() 相当于先进行Map再进行flat操作 // 相当于 [[2, 4], [3, 6], [4, 8]].flat() [2, 3, 4].flatMap((x) => [x, x * 2]) // [2, 4, 3, 6, 4, 8]
- sort() //ES2019 明确要求排序的稳定性
数组的空位
ES6中明确将空位装换成undefined;数组实例方法、map、扩展操作符存在差异
总之避免空位的出现是很必要的
对象类型
简洁写法
对象中的属性、方法只要key、value名字一致,即可简写成一个如下
// 属性
let a = 'a';
let b = 'b';
let obj = { a, b }
// 等同于
let a = 'a';
let b = 'b';
let obj = { a:a, b:b }
// 方法
let obj =
{
a,
b,
c(){
console.log('这是一个对象方法')
}
}
// 等同于
let obj =
{
a,
b,
c:function(){
console.log('这是一个对象方法')
}
}
属性名表达式
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
// 需要注意的是如果属性名是一个对象默认会装换成[object Object]
拓展操作符
// 合并对象
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);
// 同数组拓展操作符后可跟表达式 如:
const obj = {
...(x > 1 ? {a: 1} : {}),
b: 2,
};
拓展方法
可枚举性
每个属性都有一个描述对象,用来控制属性的行为Object.getOwnPropertyDescriptor(obj,'key')
可获取对应属性的描述行为,
描述中enumerable
标识是否可遍历
总之:操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替
遍历方法
- for in
-
Object.getOwnPropertyNames()
- 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
-
Object.getOwnPropertySymbols()
- 返回一个数组,包含对象自身的所有 Symbol 属性的键名。
-
Reflect.ownKeys()
- 返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
-
Object.is()
- 传统判断值是否相等使用
==
或者===
,前者会强制类型转换,后者NAN不等于自身,并+0等于-0
,此方法就解决了上述问题
- 传统判断值是否相等使用
-
Object.assign()
- Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),
第一个参数就是目标对象
,只会处理可枚举的数据类型,并Symbol值属性也会被拷贝 - 数组处理将值一一对应成对象类型
- Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),
-
Object.getOwnPropertyDescriptors()
- 对ES5中Object.getOwnPropertyDescriptor()的拓展,返回对象中所有属性(非继承)的值
-
原型操作方法替代
__proto__
- Object.setPrototypeOf()(写操作)、
- Object.getPrototypeOf()(读操作)、
- Object.create()(生成操作
-
对象键值对获取
- Object.keys(),
- Object.values(),
- Object.entries()
-
Object.fromEntries()
// Object.fromEntries()方法是Object.entries()的逆操作,用于将一个键值对数组转为对象
遍历规则如下:
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有 Symbol 键,按照加入时间升序排列。
Super 关键字
指向当前对象的原型对象。
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world"
上面代码中,super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。
ES6进阶篇
进阶篇,我们主要对新增的Symbol、ArrayBuffer
以及Set和Map
数据结构介绍、Promise 到 async
的演变和差异,异步遍历的思想,以及Class、Module的使用
;
Symbol类型
ES6引入的新的数据类型,保证了数据的独一无二的特性,可以用来标识对象的额唯一key值
// 新建类型
let a = Symbol()
// 添加描述(用于标识Symbol值)
let a = Symbol('描述文件')
特性:
- 不能new操作 注意Symbol()是一个类似string类型的值,不是对象
- 不能与其他类型值进行计算
- 可以显性的转换成字符类型/布尔值
- 座位对象的属性的时候需要注意和字符型属性,赋值、取值区别开来
方法:
- Symbol.for()、Symbol.keyFor()
返回同一个标识的Symbol类型,keyFor返回当前Symbol类型的唯一标识 - Symbol.isConcatSpreadable
判断数组concat()操作的时候是否可以会别呗展开 - Symbol.match
- Symbol.replace
- Symbol.search
- Symbol.match
- Symbol.iterator
- Symbol.toPrimitive
- Symbol.toStringTag
- Symbol.unscopables
Set、Map数据结构
Set
Set 数据结构。它类似于数组,但是成员的值都是唯一的,没有重复的值。
特性:
-
去重
Set() 去重的判断相等依据和 === 基本一致,但是在判断NAN的时候会判断相等,并且两个对象总是不相等的 -
属性
- size Set实例成员的总数
-
方法
- add()
- delete()
- has() bool值,是否是set成员
- clear() 清除所有set成员
WeakSet
和 Set
的区别
- 只能存储对象类型,不能是其他数据类型
不进入垃圾回收机制
- 没有size属性,同时不能遍历
Map
Map 的数据类型类似对象,但是对象的key值可以是任意类型的Map:值-值
,不同于传统Object:字符串-值
属性
-
属性
- size 成员总数
-
方法
- set()
- get()
- has()
- delete()
- clear()
WeakMap
和Map的结构类似
和Map
的差异
- 只接收对象作为键名
- WeakMap的键名所指向的对象,不计入垃圾回收机制
用途
在 DOM 对象上保存相关数据
-
数据缓存
const cache = new WeakMap();function countOwnKeys(obj) { if (cache.has(obj)) { console.log('Cached'); return cache.get(obj); } else { console.log('Computed'); const count = Object.keys(obj).length; cache.set(obj, count); return count; }}
- 私有变量
ArrayBuffer
操作二进制数据的一个接口。早就存在,属于独立的规格(2011 年 2 月发布),ES6 将它们纳入了 ECMAScript 规格,并且增加了新的方法。它们都是以数组的语法处理二进制数据,所以统称为二进制数组
注意:
二进制数组并不是真正的数组,而是类似数组的对象。
与Array的区别
-
属性类型
- Array 可以是基础数据类型,也可以是复杂数据类型
- ArrayBuffer 只能是 0 和 1 组成的二进制数据
-
数据存放规则
- Array 复杂数据类型存放在堆中
- ArrayBuffer 存放在栈中,读取数据更快
-
规格定义区别
- Array 无需初始定义大小,并且在使用的时候可以缩放大小
- ArrayBuffer 初始化需要定义大小,并不能再次修改
ArrayBuffer 对象
存储二进制数据的一段内存,不能直接进行读写,需要通过视图进行操作
new ArrayBuffer(32) //分配一个32位字节的内存
属性
- byteLength 返回分配区域的字节长度
方法
- slice() 操作ArrayBuffer 对象 生成一个新的内存地址
- isView() bool值返回是否是TypedArray视图实例(是否是一个视图)
TypedArray 视图
构造方法
通过构造方法生成 视图
注:
存在溢出问题
TypedArray(buffer, byteOffset=0, length?)
- TypedArray(length) // 直接分配内存
- TypedArray(typedArray) // 直接复制一个视图的值,生成一个新的视图
- TypedArray(arrayLikeObject) // 直接生成TypedArray实例
实例属性
- buffer // 返回 ArrayBuffer 对象
- byteLength // 占据内存长度(成员长度)
- byteOffset // 视图从ArrayBuffer中开始位置
- length // 字节长度
实例方法
- set() // 复制数组
- subarray() // 返回新的视图
- slice() // 返回新的视图
- of() // 用于将参数转为一个TypedArray实例
- from() // from接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray实例。 可有两个参数,第二个参数是fun
DataView 视图
实例属性
- buffer // 返回 ArrayBuffer 对象
- byteLength // 占据内存长度(成员长度)
- byteOffset // 视图从ArrayBuffer中开始位置
应用场景
- 网络请求中 blob 数据类型
-
Canvas
读取二进制像素数据 - WebSocket 传输二进制数据
-
File
new FileReader() 读取 ArrayBuffer对象
Promise/async
Promise 异步编程的解决方案,社区的提案, async 结局了Promise的回调地狱
Promise
有 padding(进行中)、fulfilled(成功)、rejected(失败)三种状态,状态已经改变就并不会变动
回调
- then() 返回结果
- catch() 捕获异常
- finally() 异步回调后都会执行,不管成功还是失败
方法
- all(<array>)
多个promise回调 等待内部所有Promise都返回成功结果/凡是一个返回reject才执行then(),多异步方法同时执行,所有都回调了再执行then() 方法 - race(<array>)
同all,区别在于,只返回最先返回结果的异步请求,不管成功还是失败 -
any()
和recede 区别是一组Promise中只有所有都reject之后才会返回失败 -
allSettled()
不同于all的点是不管成功还是失败都需要等所有异步都返回接口才会返回,所以他的状态永远是fulfilled
注意:
- 在reject、resolve中使用return,避免后续代码执行问题
- Promise.all 如果 子Promise已经拥有了 catch() 则 all() 的 catch() 不再触发
Async
async 只是对 promise 的写法上的一种语法糖
async
: 函数 返回一个Promise对象,return 返回Promise的结果
await: 后面默认是一个Promise对象
try catch(): 捕获错误
顶层await
解决异步模块加载的问题
class
将传统实例对象通过构造方法生成的过程放到class的语法糖中, 如下例
// 传统方法
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
// class 方法
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
差异点:
严格模式、不存在变量提升、this指向
取值函数(getter)和存值函数(setter)
与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
静态方法 Static
静态方法不会被继承
静态属性
//现有
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
// 提案
Static prop = 1
私有方法/私有属性
私有方法只能通过命名规则或者Symbol数据类型定义,私有属性有个提案使用的是#
Module
ES6 提出的Moduel
是前端发展过程中演变从CommonJs
递进
Module 语法
- 静态化 (编译时加载)
- 严格模式 遵循ES5的严格模式
- export 和 export default 暴露模块
- import 引入模块
- import() 解决了按需加载/条件加载/动态模块路径的问题
- export * from <moduel> 实现模块的继承
Module 的加载实现
- defer、async
defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。 - ES6 和 Commonjs 差异
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 -
循环加载问题
- Commonjs
CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。 - ES6
使用 函数 提升的优势解决报错
- Commonjs
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。