JavaScript 因为互联网而生,紧跟着浏览器的出现而问世。回顾它的历史,就要从浏览器的历史讲起。
1990年底,欧洲核能研究组织(CERN)科学家 Tim Berners-Lee,在全世界最大的电脑网络——互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。最早的网页只能在操作系统的终端里浏览,也就是说只能使用命令行操作,网页都是在字符窗口中显示,这当然非常不方便。
1992年底,美国国家超级电脑应用中心(NCSA)开始开发一个独立的浏览器,叫做 Mosaic。这是人类历史上第一个浏览器,从此网页可以在图形界面的窗口浏览。
1994年10月,NCSA 的一个主要程序员 Marc Andreessen 联合风险投资家 Jim Clark,成立了 Mosaic 通信公司(Mosaic Communications),不久后改名为 Netscape。这家公司的方向,就是在 Mosaic 的基础上,开发面向普通用户的新一代的浏览器 Netscape Navigator。
1994年12月,Navigator 发布了1.0版,市场份额一举超过90%。
Netscape 公司很快发现,Navigator 浏览器需要一种可以嵌入网页的脚本语言,用来控制浏览器行为。当时,网速很慢而且上网费很贵,有些操作不宜在服务器端完成。比如,如果用户忘记填写“用户名”,就点了“发送”按钮,到服务器再发现这一点就有点太晚了,最好能在用户发出数据之前,就告诉用户“请填写用户名”。这就需要在网页中嵌入小程序,让浏览器检查每一栏是否都填写了。
管理层对这种浏览器脚本语言的设想是:功能不需要太强,语法较为简单,容易学习和部署。那一年,正逢 Sun 公司的 Java 语言问世,市场推广活动非常成功。Netscape 公司决定与 Sun 公司合作,浏览器支持嵌入 Java 小程序(后来称为 Java applet)。但是,浏览器脚本语言是否就选用 Java,则存在争论。后来,还是决定不使用 Java,因为网页小程序不需要 Java 这么“重”的语法。但是,同时也决定脚本语言的语法要接近 Java,并且可以支持 Java 程序。这些设想直接排除了使用现存语言,比如 Perl、Python 和 TCL。
1995年,Netscape 公司雇佣了程序员 Brendan Eich 开发这种网页脚本语言。Brendan Eich 有很强的函数式编程背景,希望以 Scheme 语言(函数式语言鼻祖 LISP 语言的一种方言)为蓝本,实现这种新语言。
1995年5月,Brendan Eich 只用了10天,就设计完成了这种语言的第一版。它是一个大杂烩,语法有多个来源。
为了保持简单,这种脚本语言缺少一些关键的功能,比如块级作用域、模块、子类型(subtyping)等等,但是可以利用现有功能找出解决办法。这种功能的不足,直接导致了后来 JavaScript 的一个显著特点:对于其他语言,你需要学习语言的各种功能,而对于 JavaScript,你常常需要学习各种解决问题的模式。而且由于来源多样,从一开始就注定,JavaScript 的编程风格是函数式编程和面向对象编程的一种混合体。
Netscape 公司的这种浏览器脚本语言,最初名字叫做 Mocha,1995年9月改为 LiveScript。12月,Netscape 公司与 Sun 公司(Java 语言的发明者和所有者)达成协议,后者允许将这种语言叫做 JavaScript。这样一来,Netscape 公司可以借助 Java 语言的声势,而 Sun 公司则将自己的影响力扩展到了浏览器。
之所以起这个名字,并不是因为 JavaScript 本身与 Java 语言有多么深的关系(事实上,两者关系并不深,详见下节),而是因为 Netscape 公司已经决定,使用 Java 语言开发网络应用程序,JavaScript 可以像胶水一样,将各个部分连接起来。当然,后来的历史是 Java 语言的浏览器插件失败了,JavaScript 反而发扬光大。
1995年12月4日,Netscape 公司与 Sun 公司联合发布了 JavaScript 语言,对外宣传 JavaScript 是 Java 的补充,属于轻量级的 Java,专门用来操作网页。
1996年3月,Navigator 2.0 浏览器正式内置了 JavaScript 脚本语言。
要讲清楚这个问题,需要回顾历史。1996年11月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给国际标准化组织ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript ,这个版本就是1.0版。
该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript ,有两个原因。一是商标,Java是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA ,不是 Netscape ,这样有利于保证这门语言的开放性和中立性。
因此,ECMAScript 和 JavaScript 的关系是,ECMAScript 是一个简单的 JavaScript 标准规范,JavaScript 是 ECMAScript 的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript )。并且,ECMAScript 持续不断的为 JavaScript 添加新功能。
从 1997年7月 ECMAScript 1.0发布到现在,ECMAScript 已经正式发布了 11 版,下面我们主要介绍从ES6(ES2015)到ES11(最新 ES2020 )期间,每版发布的新功能。
ES6的特性比较多,在 ES5 发布近 6 年(2009-11 至 2015-6)之后才将其标准化。两个发布版本之间时间跨度很大,所以ES6中的特性比较多。 在这里列举几个常用的:
function foo(age = 25,){ // ...}
const name = '小豪';
const str = `Your name is ${name}`;
[1].includes(1); // true
2**10; // 1024
异步终极解决方案
async getData(){
const res = await api.getTableData(); // await 异步任务
// do something
}
Object.values({a: 1, b: 2, c: 3}); // [1, 2, 3]
Object.entries({a: 1, b: 2, c: 3}); // [["a", 1], ["b", 2], ["c", 3]]
// padStart
'hello'.padStart(10); // " hello"
// padEnd
'hello'.padEnd(10) "hello "
返回 obj对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
const obj2 = {
name: 'Jine',
get age(){
return '18'
}
};
Object.getOwnPropertyDescriptors(obj2)
// age: {
// enumerable: true,
// set: undefined
// name: {
// enumerable: true,
// writable:true
// }
SharedArrayBuffer 对象用来表示一个通用的,固定长度的原始二进制数据缓冲区,类似于 ArrayBuffer 对象,它们都可以用来在共享内存(shared memory)上创建视图。与 ArrayBuffer 不同的是,SharedArrayBuffer 不能被分离。
/**
*
* @param {*} length 所创建的数组缓冲区的大小,以字节(byte)为单位。
* @returns {SharedArrayBuffer} 一个大小指定的新 SharedArrayBuffer 对象。其内容被初始化为 0。
*/
new SharedArrayBuffer(10)
Atomics 对象提供了一组静态方法用来对 SharedArrayBuffer 对象进行原子操作。
这些原子操作属于 Atomics 模块。与一般的全局对象不同,Atomics 不是构造函数,因此不能使用 new 操作符调用,也不能将其当作函数直接调用。Atomics 的所有属性和方法都是静态的(与 Math 对象一样)。
多个共享内存的线程能够同时读写同一位置上的数据。原子操作会确保正在读或写的数据的值是符合预期的,即下一个原子操作一定会在上一个原子操作结束后才会开始,其操作过程不会中断。
将指定位置上的数组元素与给定的值相加,并返回相加前该元素的值。
将指定位置上的数组元素与给定的值相与,并返回与操作前该元素的值。
- Atomics.compareExchange()
- Atomics 对象提供了一组静态方法用来对 SharedArrayBuffer 对象进行原子操作。
如果数组中指定的元素与给定的值相等,则将其更新为新的值,并返回该元素原先的值。
将数组中指定的元素更新为给定的值,并返回该元素更新前的值。
返回数组中指定元素的值。
将指定位置上的数组元素与给定的值相或,并返回或操作前该元素的值。
将数组中指定的元素设置为给定的值,并返回该值。
将指定位置上的数组元素与给定的值相减,并返回相减前该元素的值。
将指定位置上的数组元素与给定的值相异或,并返回异或操作前该元素的值。
wait() 和 wake() 方法采用的是 Linux 上的 futexes 模型(fast user-space mutex,快速用户空间互斥量),可以让进程一直等待直到某个特定的条件为真,主要用于实现阻塞。
检测数组中某个指定位置上的值是否仍然是给定值,是则保持挂起直到被唤醒或超时。返回值为 “ok”、“not-equal” 或 “time-out”。调用时,如果当前线程不允许阻塞,则会抛出异常(大多数浏览器都不允许在主线程中调用 wait())。
唤醒等待队列中正在数组指定位置的元素上等待的线程。返回值为成功唤醒的线程数量。
可以用来检测当前系统是否支持硬件级的原子操作。对于指定大小的数组,如果当前系统支持硬件级的原子操作,则返回 true;否则就意味着对于该数组,Atomics 对象中的各原子操作都只能用锁来实现。此函数面向的是技术专家。–>
await可以和for…of循环一起使用,以串行的方式运行异步操作
async function process(array) {
for (leti of array) {
await doSomething(i);
}
}
这段代码不会正常运行
ES2018引入异步迭代器(asynchronous iterators),这就像常规迭代器,除了 next()方法返回一个Promise。因此 await可以和 for…of循环一起使用,以串行的方式运行异步操作。例如:
async function process(array) {
for await (let i of array) {
doSomething(i);
}
}
一个Promise调用链要么成功到达最后一个 .then(),要么失败触发 .catch()。在某些情况下,你想要在无论Promise运行成功还是失败,运行相同的代码,例如清除,删除对话,关闭数据库连接等。
.finally()允许你指定最终的逻辑:
Promise.resolve().then().catch(e => e).finally();
ES2015引入了Rest参数和扩展运算符。三个点(…)仅用于数组。Rest参数语法允许我们将一个不定数量的参数表示为一个数组。
const values = [1, 2, 3, 5, 6];
console.log( Math.max(...values) ); // 6
JavaScript正则表达式可以返回一个匹配的对象——一个包含匹配字符串的类数组,例如:以 YYYY-MM-DD的格式解析日期:
const reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match[1],
month = match[2],
day = match[3];
这样的代码很难读懂,并且改变正则表达式的结构有可能改变匹配对象的索引。
ES2018允许命名捕获组使用符号 ?,在打开捕获括号 (后立即命名,示例如下:
const reDate = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/ ,
match = reDate.exec('2018-04-30'),
year = match.groups.year,
month = match.groups.month, // 04
day = match.groups.day;
任何匹配失败的命名组都将返回 undefined。
命名捕获也可以使用在 replace()方法中。例如将日期转换为美国的 MM-DD-YYYY 格式:
const reDate = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/ ,
d = '2018-04-30',
usDate = d.replace(reDate, '$-$-$' );
不清楚后续去了解
以前,这些符号在字符串文字中被视为行终止符,因此使用它们会导致SyntaxError异常。
如果输入 Unicode 格式但是超出范围的字符,在原先JSON.stringify返回格式错误的Unicode字符串。现在实现了一个改变JSON.stringify的第3阶段提案,因此它为其输出转义序列,使其成为有效Unicode(并以UTF-8表示)
flat()和 flatMap()本质上就是是归纳(reduce) 与 合并(concat)的操作。
[1, 2, [3, 4]].flat(Infinity); // [1, 2, 3, 4]
[1, 2, 3, 4].flatMap(a => [a**2]); // [1, 4, 9, 16]
新增的这两个方法很好理解,分别去除字符串首尾空白字符,这里就不用例子说声明了。
Object.entries()方法的作用是返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for…in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环也枚举原型链中的属性)。
而 Object.fromEntries() 则是 Object.entries() 的反转。
Object.fromEntries() 函数传入一个键值对的列表,并返回一个带有这些键值对的新对象。这个迭代参数应该是一个能够实现@iterator方法的的对象,返回一个迭代器对象。它生成一个具有两个元素的类似数组的对象,第一个元素是将用作属性键的值,第二个元素是与该属性键关联的值。
// 通过 Object.fromEntries, 可以将 Map 转化为 Object:
const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
console.log(Object.fromEntries(map)); // { foo: "bar", baz: 42 }
通过 Object.fromEntries, 可以将 Array 转化为 Object:
const arr = [['0', 'a'], ['1', 'b'], ['2', 'c']];
const obj = Object.fromEntries(arr);
console.log(obj);//{0: "a", 1: "b", 2: "c"}
只读属性,回 Symbol 对象的可选描述的字符串。
Symbol('description').description; // 'description'
matchAll()为所有匹配的匹配对象返回一个迭代器
const raw_arr = 'test1 test2 test3'.matchAll((/t(e)(st(\d?))/g));
const arr = [...raw_arr];
function /* comment */ foo /* another comment */() { }
console.log(foo.toString());
// ES2019 会把注释一同打印
console.log(foo.toString());
在 ES10 之前,我们必须通过语法为 catch 子句绑定异常变量,无论是否有必要。很多时候 catch 块是多余的。ES10 提案使我们能够简单的把变量省略掉。
不算大的改动。
Promise.all 缺陷都知道 Promise.all 具有并发执行异步任务的能力。但它的最大问题就是如果其中某个任务出现异常(reject),所有任务都会挂掉,Promise 直接进入 reject 状态。
我们需要一种机制,如果并发任务中,无论一个任务正常或者异常,都会返回对应的的状态(fulfilled 或者 rejected)与结果(业务 value 或者 拒因 reason),在 then 里面通过 filter 来过滤出想要的业务逻辑结果,这就能最大限度的保障业务当前状态的可访问性,而 Promise.allSettled 就是解决这问题的。
Promise.allSettled([
Promise.reject({code: 500, msg: '服务异常'}),
Promise.resolve({ code: 200, list: []}),
Promise.resolve({code: 200, list: []})
])
.then((ret) => {
/*
0: {status: "rejected", reason: {...}}
1: {status: "fulfilled", value: {...}}
2: {status: "fulfilled", value: {...}}
*/
// 过滤掉 rejected 状态,尽可能多的保证页面区域数据渲染
RenderContent(ret.filter((el) => {
return el.status !== 'rejected';
}));
});
可选链 可让我们在查询具有多层级的对象时,不再需要进行冗余的各种前置校验。
日常开发中,我们经常会遇到这种查询
var name = user && user.info && user.info.name;
这是一种丑陋但又不得不做的前置校验,否则很容易命中 Uncaught TypeError: Cannot read property… 这种错误,这极有可能让你整个应用挂掉。
用了 Optional Chaining ,上面代码会变成
var name = user?.info?.name;
当我们查询某个属性时,经常会遇到,如果没有该属性就会设置一个默认的值。比如下面代码中查询玩家等级。
var level = (user.data && user.data.level) || '暂无等级';
在 JS 中,空字符串、0 等,当进行逻辑操作符判断时,会自动转化为 false。在上面的代码里,如果玩家等级本身就是 0 级, 变量 level 就会被赋值 暂无等级 字符串,这是逻辑错误。
来看看用空值合并运算符如何处理
var level = `${user.level}级` ?? '暂无等级';
可选链合用
var level = user.data?.level ?? '暂无等级';
按需 import 提案几年前就已提出,如今终于能进入 ES 正式规范。这里个人理解成 “按需” 更为贴切。现代前端打包资源越来越大,打包成几 M 的 JS 资源已成常态,而往往前端应用初始化时根本不需要全量加载逻辑资源,为了首屏渲染速度更快,很多时候都是按需加载,比如懒加载图片等。而这些按需执行逻辑资源都体现在某一个事件回调中去加载。
el.onclick = () => {
import(`/path/current-logic.js`)
.then((module) => {
module.doSomthing();
})
.catch((err) => {
// load error;
})
}
当然,webpack 目前已很好的支持了该特性
JavaScript 在不同的环境获取全局对象有不同的方式,NodeJS 中通过 global, Web 中通过 window, self 等,有些甚至通过 this 获取,但通过 this 是及其危险的,this 在 JavaScript 中异常复杂,它严重依赖当前的执行上下文,这些无疑增加了获取全局对象的复杂性
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { returnwindow; }
if (typeof global !== 'undefined') { return global; }
thrownewError('unable to locate global object');
};
var globals = getGlobal();
而 globalThis 目的就是提供一种标准化方式访问全局对象,有了 globalThis后,你可以在任意上下文,任意时刻都能获取到全局对象。
JavaScript 中 Number 类型只能安全的表示-(2^53-1)至 2^53-1 范的值,即 Number.MIN_SAFE_INTEGER 至 Number.MAX_SAFE_INTEGER,超出这个范围的整数计算或者表示会丢失精度。
var num = Number.MAX_SAFE_INTEGER; // -> 9007199254740991
num = num + 1; // -> 9007199254740992
// 再次加 +1 后无法正常运算
num = num + 1; // -> 9007199254740992
// 两个不同的值,却返回了true
9007199254740992 === 9007199254740993// -> true
为解决此问题,ES2020 提供一种新的数据类型:BigInt。使用 BigInt 有两种方式:
var bigIntNum = 9007199254740993n;
var bigIntNum = BigInt(9007199254740);
var anOtherBigIntNum = BigInt('9007199254740993');
通过 BigInt, 我们可以安全的进行大数整型计算。
var bigNumRet = 9007199254740993n + 9007199254740993n; // -> -> 18014398509481986n
bigNumRet.toString(); // -> '18014398509481986'
注意:
typeof9007199254740993n; // -> 'bigint'
返回一个全新的字符串,所有符合匹配规则的字符都将被替换掉
const str = 'hello world';
str.replaceAll('l', ''); // "heo word"
Promise.any() 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise 。如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise
const promise1 = new Promise((resolve, reject) => reject('我是失败的Promise_1'));
const promise2 = new Promise((resolve, reject) => reject('我是失败的Promise_2'));
const promiseList = [promise1, promise2];
Promise.any(promiseList)
.then(values=>{
console.log(values);
})
.catch(e=>{
console.log(e);
});
![](https://img-blog.csdnimg.cn/img_convert/2c8c2d9e9a2f2808ff52e8430fbd091d.png#clientId=u4d8de730-31ae-4&from=paste&height=100&id=u068df3f2&margin=[object Object]&originHeight=100&originWidth=657&originalType=url&status=done&style=none&taskId=uf1e8675a-ef8e-4faa-b8cc-7a30974021e&width=657)
使用WeakRefs的Class类创建对对象的弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)
逻辑运算符和赋值表达式,新特性结合了逻辑运算符(&&,||,??)和赋值表达式而JavaScript已存在的 复合赋值运算符有:
a ||= b
//等价于
a = a || (a = b)
a &&= b
//等价于
a = a && (a = b)
a ??= b
//等价于
a = a ?? (a = b)
数字分隔符,可以在数字之间创建可视化分隔符,通过_下划线来分割数字,使数字更具可读性
const money = 1_000_000_000;
//等价于
const money = 1000000000;
1_000_000_000 === 1000000000; // true
ES6、ES7、ES8、ES9、ES10新特性一览
https://juejin.cn/post/6844903811622912014
ES11新特性介绍
https://juejin.cn/post/6844904180855881736
JavaScript ES12新特性抢先体验
https://juejin.cn/post/6890652568507121677