责编 | 郭芮

出品 | CSDN(ID:CSDNnews)

JavaScript能发展到现在的程度已经经历不少的坎坷,早产带来的某些毛病是永久性的,因此浏览器才有禁用JavaScript的选项。
乃至在jQuery时期有人问出这样的问题,jQuery与JavaScript哪个快?在Babel.js出来之前,发明一门全新的措辞代码代替JavaScript的呼声一贯不绝于耳,前有VBScript,Coffee, 后有Dartjs, WebAssembly。
要不是它是所有浏览器都内置的脚本措辞, 可能就命绝于此。
浏览器便是它的那个有钱的丈母娘。
此外源源不断的类库框架,则是它的武器库,从底层改造了它自己。
为什么这么说呢?

jspsyntaxvalidatorJavaScript 为什么能活到如今 JavaScript

JavaScript没有其他措辞那样弘大的SDK,针对某一个领域自带的方法是很少,比如说数组方法,字符串方法,都不超过20个,是Prototype.js给它加上的。
JavaScript要实现页面动效,离不开DOM与BOM,但浏览器相互竞争,导致API不一致,是jQuery搞定了,还带来了链式调用与IIFE这些新的编程技巧。
在它缺少大规模编程模式的时候,其他措辞的外来户又给它带来了MVC与MVVM……这里面许多东西,久而久之都变成措辞内置的特性,比如Prototype.js带来的原型方法,jQuery带来的选择器方法,实现MVVM不可短缺的工具属性自察机制(getter, setter, Reflect, Proxy), 大规模编程须要的class, modules。

本文将以下几个方面先容这些新特性,正是它们武装了JavaScript,让它变成一个正统的,魔幻的措辞。

原型方法的极大丰富;

类与模块的标准化;

异步机制的嬗变;

块级浸染域的补完;

根本类型的增加;

反射机制的完善;

更顺手的语法糖。

原型方法的极大丰富

原型方法自Prototype.js出来后,就不断被招抚成官方API。
基本上在字符串与数组这两大种别扩充,它们在日常业务中不断被利用,因此不断变重复造轮子,因此企待官方化。

JavaScript的版本解释:

这些原型方法非常有用,甚至于在口试中常常被问到,如果去除字符串两边的空缺,如何扁平化一个数组?

类与模块的标准化

在没有类的时期,每个盛行框架都会带一个创建类的方法,可见大家都不太认同原型这种复用机制。

下面是原型与类的写法比较:

function Person(name) {

this.name = name;

}

//定义一个方法并且赋值给布局函数的原型

Person.prototype.sayName = function {

return this.name;

};

var p = new Person('ruby');

console.log(p.sayName) // ruby

class Person {

constructor(name){

this.name = name

}

sayName {

return this.name;

}

}

var p = new Person('ruby');

console.log(p.sayName) // ruby

我们可以看到es6的定义是非常大略的,并且不同于工具键值定义办法,它是利用工具简写来描述方法。
如果是标准的工具描述法,该当是这样:

//下面这种写法并不合法

class Person {

constructor: function(name){

this.name = name

}

sayName: function {

return this.name;

}

}

如果我们想继续一个父类,也很大略:

class Person extends Animal {

constructor: function(name){

super;

this.name = name

}

sayName: function {

return this.name;

}

}

此外,它后面还补充了三次干系的语法,分别是属性初始化语法,静态属性与方法语法,私有属性语法。
目前私有属性语法争议非常大,但还是被标准化。
虽然像typescript的private、public、protected更符合从后端转行过来的人的口味,不过在babel无所不能的本日,我们完备可以利用自己喜好的写法。

与类一起涌现的还有模块,这是一种比类更大的复用单元,以文件为载体,可以实现按需加载。
当然它最紧张的浸染是减少全局污染。
jQuery时期,通过IIFE减少了这症状,但是JS文件没有统一的编写规范,意味着想把它们打包一个是非常困难的,只能像下面那样平铺着。
这些文件的依赖关系,只有最初的人知道,要了几轮开拓后,便是定时炸弹。
此外,不要忘却,<script&gt;标准还会导致页面渲染堵塞,涌现白屏征象。

<script src=\公众zepto.js\公众></script>

<script src=\公众jhash.js\公众></script>

<script src=\"大众fastClick.js\公众></script>

<script src=\"大众iScroll.js\公众></script>

<script src=\"大众underscore.js\"大众></script>

<script src=\"大众handlebar.js\"大众></script>

<script src=\"大众datacenter.js\公众></script>

<script src=\"大众util/wxbridge.js\公众></script>

<script src=\"大众util/login.js\"大众></script>

<script src=\"大众util/base.js\"大众></script>

于是后jQuery时期,海内盛行三种模块机制,以seajs主体的CMD,以requirejs为主体的AMD,及nodejs自带的Commonjs。
当然,后来还有一种三合一方案UMD(AMD, Commonjs与es6 modules)。

requirejs的定义与利用:

define(['jquery'], function($){

//some code

var mod = require(\"大众./relative/name\"大众);

return {

//some code

} //返回值可以是工具、函数等

})

require(['cores/cores1', 'cores/cores2', 'utils/utils1', 'utils/utils2'], function(cores1, cores2, utils1, utils2){

//some code

})

requirejs是天下第一款通用的模块加载器,尤其自创了shim机制,让许多不模范的JS文件也可以纳入其加载系统。

define(function(require){

var $ = require(\公众jquery\"大众);

$(\"大众#container\公众).html(\公众hello,seajs\公众);

var service = require(\公众./service\"大众)

var s = new service;

s.hello;

});

//另一个独立的文件service.js

define(function(require,exports,module){

function Service{

console.log(\公众this is service module\"大众);

}

Service.prototype.hello = function{

console.log(\公众this is hello service\公众);

return this;

}

module.exports = Service;

});

Seajs是阿里大牛玉伯加的加载器,借鉴了Requiejs的许多功能,听说其性能与严谨性超过前者。
当前为了精确剖析出define回调里面的require语句,还发起了一个 100 美刀赏金活动,让海内高手一展技艺。

https://github.com/seajs/seajs/issues/478

image_1doan2vfl17ld1nin1hbm182c9b9p.png-72.9kB

相对而言,nodejs模块系统就大略多了,它没有专门用于包裹用户代码的define方法,它不须要显式声明依赖。

//world.js

exports.world = function {

console.log('Hello World');

}

//main.js

let world = require('./world.js')

world;

function Hello {

var name;

this.setName = function(thyName) {

name = thyName;

};

this.sayHello = function {

console.log('Hello ' + name);

};

};

module.exports = Hello;

而官方钦点的es6 modules与nodejs模块系统极其相似,只是将其方法与工具变成关键字。

//test.js或test.mjs

import as test from './test';

//aaa.js或aaa.mjs

import {aaa} from \"大众./aaa\"大众

const arr = [1, 2, 3, 4];

const obj = {

a: 0,

b: function {}

}

export const foo = => {

const a = 0;

const b = 20;

return a + b;

}

export default {

num,

arr,

obj,

foo

}

那怎么利用呢?根据规范,浏览器须要在link标签与script标签添加新的属性或属性值来支持这新特性。
(详见:https://www.jianshu.com/p/f7db50cf956f)

<link rel=\"大众modulepreload\公众 href=\公众lib.mjs\"大众>

<link rel=\"大众modulepreload\"大众 href=\"大众main.mjs\"大众>

<script type=\公众module\"大众 src=\"大众main.mjs\"大众></script>

<script nomodule src=\"大众fallback.js\公众></script>

但可惜的是,浏览器对模块系统的支持是非常滞后,并且即便最新的浏览器支持了,我们还是免不了要兼容旧的浏览器。
对此,我们只能奠出webpack这利器,它是前端工程化的集大成者,可以将我们的代码通过各种loader/plugin打包成主流浏览器都认识的JavaScript语法,并以最原始的办法挂载进去。

异步机制的嬗变

在JavaScript没有大规模运用前,用到异步的地方只有ajax要求与动画,在要求结束与动画结束时要做什么事,利用的办法是经典的回调。

回调

由于javascript是单线程的,我们的方法是同步的,像下面这样,一个个实行:

A;

B;

C;

而异步则是不可预测其触发机遇:

A;

// 在现在发送要求

ajax({

url: url,

data: {},

success:function(res){

// 在未来某个时候实行

B(res)

}

})

C;

//实行顺序:A -> C -> B

回调函数是主函数的后继方法,基本上能担保,主函数实行后,它能在之后某个时候被实行一次。
但随着功能的细分,在微信小程序或快运用中,它们拆分成三个,即一个方法随着三个回调。

// https://doc.quickapp.cn/features/system/share.html

import share from '@system.share'

share.share({

type: 'text/html',

data: '<b>bold</b>',

success: function{},

fail: function{},

complete: function{}

})

在nodejs中,内置的异步方法都是利用一种叫Error-first回调模式。

fs.readFile('/foo.txt', function(err, data) {

// TODO: Error Handling Still Needed!

console.log(data);

});

在后端,由于存在IO操作,异步操作非常多,异步套异步很随意马虎造成回调地狱。
于是涌现了另一种模式,事宜中央,EventBus或EventEmiiter。

var EventEmitter = require('events').EventEmitter;

var ee = new EventEmitter;

ee.on('some_events', function(foo, bar) {

console.log(\公众第1个监听事宜,参数foo=\"大众 + foo + \"大众,bar=\公众+bar );

});

console.log('第一轮');

ee.emit('some_events', 'Wilson', 'Zhong');

console.log('第二轮');

ee.emit('some_events', 'Wilson', 'Z');

事宜可以一次绑定,多次触发,并且可以将原来内部的回调拖出来,有效地避免了回调地狱。
但事宜中央,对付同一种行为,总是解发一种回调,不能像小程序的回调那么清晰。
于是jQuery引进了Promise。

Promise

Promise最初叫Deffered,从Python的Twisted框架中引进过来。
它通过异步办法完成用类的构建,又通过链式调用办理了回调地狱问题。

var p = new Promise(function(resolve, reject){

console.log(\公众========\"大众)

setTimeout(function{

resolve(1)

},300)

setTimeout(function{

//reject与resolve只能二选一

reject(1)

},400)

});

console.log(\"大众这个先实行\"大众)

p.then(function (result) {

console.log('成功:' + result);

})

.catch(function (reason) {

console.log('失落败:' + reason);

}).finally(function{

console.log(\公众总会实行\"大众)

})

为什么这么说呢?看上面的示例,new Promise(executor)里的executor方法,它会待到then, catch, finally等方法添加完,才会实行,它是异步的。
而then, catch, finally则又恰好对应success, fail, complete这三种回调,我们可以为Promise以链式办法添加多个then方法。

如果你不想写catch,新锐的浏览器还供应了一个新事宜做统一处理:

window.addEventListener('unhandledrejection', function(event) {

// the event object has two special properties:

alert(event.promise); // [object Promise] - 产生缺点的 promise

alert(event.reason); // Error: Whoops! - 未处理的缺点工具

});

new Promise(function {

throw new Error(\"大众Whoops!\"大众);

}); // 没有 catch 处理缺点

nodejs也有相同的事宜:

process.on('unhandledRejection', (reason, promise) => {

console.log('未处理的谢绝:', promise, '缘故原由:', reason);

// 记录日志、抛出错误、或其他逻辑。

});

除此之外,esma2020年还为Promise添加了三个静态方法:Promise.all和Promise.race,Promise.allSettled 。

实在chrome 60已经都可以用了。

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失落败(rejected),此实例回调失落败(reject),失落败缘故原由的是第一个失落败 promise 的结果。

var promise1 = Promise.resolve(3);

var promise2 = 42;

var promise3 = new Promise(function(resolve, reject) {

setTimeout(resolve, 100, 'foo');

});

Promise.all([promise1, promise2, promise3]).then(function(values) {

console.log(values);

});

// expected output: Array [3, 42, \公众foo\公众]

这个方法类似于jQuery.when,专门用于处理并发事务。

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise办理或谢绝,返回的 promise就会办理或谢绝。
此方法用于竞态的情形。

Promise.allSettled(iterable)方法返回一个promise,该promise在所有给定的promise已被解析或被谢绝后解析,并且每个工具都描述每个promise的结果。
它类似于Promise.all,但不会由于一个reject就会实行后继回调,必须所有promise都被实行才会。

Promise不并比EventBus, 回调等精良,但是它给前端API供应了一个标杠,往后处理异步便是返回一个Promise。
为后来async/await做了铺垫。

天生器

天生器generator, 不是为办理异步问题而出身的,只是恰好它的某个特性可以解耦异步的繁芜性,加之koa的暴红,人们创造原来generator还可以这样用,于是就火了。

为了理解天生器的含义,我们须要先理解迭代器,迭代器中的迭代便是循环的意思。
比如es5中的forEach, map, filter便是迭代器。

let numbers = [1, 2, 3];

for (let i = 0; i < numbers.length; i++) {

console.log(numbers[i]);

}

//它比上面更精简

numbers.forEach(function(el){

console.log(el);

})

但forEach会一下子把所有元素都遍历出来,而我们喜好一个个处理呢?那我们就要手写一个迭代器。

function makeIterator(array){

var nextIndex = 0;

return {

next: function{

return nextIndex < array.length ?

{value: array[nextIndex++], done: false} :

{done: true};

}

};

}

var it = makeIterator([1,2,3])

console.log(it.next); // {value: 1, done: false}

console.log(it.next); // {value: 2, done: false}

console.log(it.next); // {value: 3, done: false}

console.log(it.next); // {done: true}

而天生器则将创建迭代器常用的模式官方化,就像创建类一样,但是它写法有点怪,不像类那样专门弄一个关键字,也没有像Promise那样弄一个类。

//空想中是这样的

Iterator{

exector{

yield 1;

yield 2;

yield 3;

}

}

//现实是这样的

function Iterator {

yield 1;

yield 2;

yield 3;

}

实在最好是像Promise那样,弄一个类,那么我们还可以用现成的语法来仿照,但天生器,现在一个新关键字yield,你可以将它当一个return语句。
天生器实行后,会产生一个工具,它有一个next方法,next方法实行多少次,就轮到第几个yield的值返回。

function Iterator {

yield 1;

yield 2;

yield 3;

}

let it = Iterator;

console.log(it.next); // {value: 1, done: false}

console.log(it.next); // {value: 2, done: false}

console.log(it.next); // {value: 3, done: false}

console.log(it.next); // {value: undefined, done: true}

由于写法比较离经背道,因此常日见于类库框架,业务中很少有人利用。
它涉及许多细节,比如说yield与return的混用。

function generator {

yield 1;

return 2; //这个被转换成 yield 2, 并立即设置成done: true

yield 3; //这个被忽略

}

let it = generator;

console.log(it.next); // {value: 1, done: false}

console.log(it.next); // {value: 2, done: true}

console.log(it.next); // {value: undefined, done: true}

image_1doda17jkj7kl4u1qru1era2m316.png-322.9kB

但说了这么多,这与异步有什么关系呢?我们之以是须要回调,事宜,Promise这些,实在是希望能实现以同步代码的办法组件异步逻辑。
yield相称一个断点,能中断程序往下实行。
于是异步的逻辑就可以这样写:

function generator {

yield setTimeout(function{ console.log(\"大众111\"大众), 200})

yield setTimeout(function{ console.log(\公众222\"大众), 100})

}

let it = generator;

console.log(it.next); // 1 视浏览器有所差异

console.log(it.next); // 2 视浏览器有所差异

如果没有yield,肯定是先打出222,再打出111。

好了,我们搞定异步代码以同步代码的顺序输出后,就处理手动实行next方法的问题。
这个也大略,写一个方法,用程序实行它们。

function timeout(data, time){

return new Promise(function(resolve){

setTimeout(function{

console.log(data, new Date - 0)

resolve(data)

},time)

})

}

function generator{

let p1 = yield timeout(1, 2000)

console.log(p1)

let p2 = yield timeout(2, 3000)

console.log(p2)

let p3 = yield timeout(3, 2000)

console.log(p3)

return 2;

}

// 按顺序输出 1 2 3

/ 传入要实行的gen /

/ 实在循环遍历所有的yeild (函数的递归)

根绝next返回值中的done判断是否实行到末了一个,

如果是末了一个则跳出去/

function run(fn) {

var gen = fn;

function next(data) {

// 实行gen.next 初始data为undefined

var result = gen.next(data)

// 如果result.done 为true

if(result.done) {

return result.value

}else{

// result.value 为promise

result.value.then(val=>{

next(val)

})

}

}

// 调用上一个next方法

next;

}

run(generator)

koa早些年的版本依赖的co库,便是基于上述事理摆平异步问题。
有兴趣的同学可以下来看看。

async/await

上节章的天生器已经完美地办理异步的逻辑以同步的代码编写的问题了,什么非常,可以直接try catch,成功则直接往下走,总是实行可以加finally语句,美中不敷是须要对yield后的方法做些改造,改成Promise(这个也有库,在nodejs直接内置了util.promisefy)。
然后须要一个run方法,代替手动next。
于是处于措辞供应链上流的大佬们想,能不能直接将这两步内置呢?然后包装一个已经被人接管的语法供应给没有见过世面的前端工程师呢?他们搜刮了一遍,还真有这东西。
那便是C#有async/await。

//C# 代码

public static async Task<int> AddAsync(int n, int m) {

int val = await Task.Run( => Add(n, m));

return val;

}

这种没有学习本钱的语法很快迁移到JS中,async关键字,相称于天生器函数与我们自造的实行函数,await关键字相称于yield,但它只有在它随着的是Promise才会中断流程实行。
async函数末了会返回一个Promise,可以供表面的await关键字利用。

//javascript 代码

async function addTask {

await new Promise(function(resolve){

setTimeout(function{ console.log(\"大众111\公众); resolve, 200})

})

console.log('222')

await new Promise(function(resolve){

setTimeout(function{ console.log(\公众333\"大众); resolve, 200})

})

console.log('444')

}

var p = addTask

console.log(p)

image_1dodd79nc1imnnm91q1b1p7qhdp1j.png-6.1kB

在循环中利用async/await:

const array = [\公众a\公众,\公众b\公众, \公众c\"大众]

function getNum(num){

return new Promise(function(resolve){

setTimeout(function{

resolve(num)

}, 300)

})

}

async function asyncLoop {

console.log(\"大众start\公众)

for(let i = 0; i < array.length; i++){

const num = await getNum(array[i]);

console.log(num, new Date-0)

}

console.log(\公众end\公众)

}

asyncLoop

async函数里面的缺点也可以用try catch包住,也可以利用上面提到的unhandledrejection方法。

async function addTask {

try{

await ...

console.log('222')

}catch(e){

console.log(e)

}

}

此外,es2018还添加了异步迭代器与异步天生器函数,让我们处理各种异步场景更加得心应手:

//异步迭代器

const ruby = {

[Symbol.asyncIterator]: => {

const items = [`r`, `u`, `b`, `y`, `l`, `o`,`u`, `v`, `r`, `e`];

return {

next: => Promise.resolve({

done: items.length === 0,

value: items.shift

})

}

}

}

for await (const item of ruby) {

console.log(item)

}

//异步天生器函数,async函数与天生器函数的稠浊体

async function readLines(path) {

let file = await fileOpen(path);

try {

while (!file.EOF) {

yield await file.readLine;

}

} finally {

await file.close;

}

}

块级浸染域的补完

提及浸染域,大家一样平常认为JavaScript只有全局浸染域与函数浸染域,但是es3时期,它还是能通过catch语句与with语句创造块级浸染域的。

try{

var name = 'global' //全局浸染域

}catch(e){

var b = \"大众xxx\"大众

console.log(b)//xxx

}

console.log(b)

var obj = {

name: \公众block\公众

}

with(obj) {

console.log(name);//Block块上的name block

}

console.log(name)//global

但是catch语句实行后,还是会污染表面的浸染域,并且catch是很耗性能的。
而with更不用说了,会引起歧义,被es5严格模式禁止了。

话又说回来,之以是须要块状浸染域,是用来办理es3的两个不好的设计,一个是变量提升,一个重复定义,它们都不利于团队协作与大规模生产。

var x = 1;

function rain{

alert( x ); //弹出 'undefined',而不是1

var x = 'rain-man';

alert( x ); //弹出 'rain-man'

}

rain;

因此到es6中,新添了let和const关键字来实现块级浸染域。
这两个关键字比较var,有如下特点:

浸染域是局部,浸染范围是括起它的两个花括号间,即for{},while{},if{}与纯挚的{}

它也不会提升到浸染域顶部,它顶部到定义的那一行变称之为“暂时性去世区”,这时利用它会报错。

变量一旦变let, const声明,就再不能重复定义,否则也报错。
这种严格的缺点提示对我们调试是非常有帮助的。

let a = \"大众hey I am outside\"大众;

if(true){

//此处存在暂时性去世区

console.log(a);//Uncaught ReferenceError: a is not defined

let a = \"大众hey I am inside\"大众;

}

//let与const不存在变量提升

console.log(a); // Uncaught ReferenceError: a is not defined

console.log(b); // Uncaught ReferenceError: b is not defined

let a = 1; //Uncaught SyntaxError: Identifier 'a' has already been declared

const b = 2;

//不存在变量提升,因此块级浸染域外层无法访问

if(true){

var bar = \"大众bar\"大众;

let baz = \"大众baz\公众;

const qux = \"大众qux\公众;

}

console.log(bar);//bar

console.log(baz);//baz is not defined

console.log(qux);//qux is not defined

const声明则比let声明多了一个功能,就让目标变量的值不能再次改变,即其他措辞的常量。

根本类型的增加

在javascript, 我们通过typeof与Object.prototype.toString.call可以区分出工具的类型,过去总有7种类型:undefined, , string, number, boolean, function, object。
现在又多出两个类型,一个是es6引进的Symbol,另一个是es2019的bBigInt。

console.log(typeof 9007199254740991n); // \公众bigint\"大众

console.log(typeof Symbol(\"大众aaa\公众)); // \"大众symbol\"大众

Symbol拥有三个特性,创建的值是独一无二的,附加在工具是不可遍历的,不支持隐式转换。
此外Symbol上面还有其他静态方法,用来为工具扩展更多功能。

我们先看它如何表示独一无二的属性值。
如果没有Symbol,我们平凡表示常量的方法是不可靠的。

const COLOR_GREEN = 1

const COLOR_RED = 2

const LALALA = 1;

function isSafe(args) {

if (args === COLOR_RED) return false

if (args === COLOR_GREEN) return true

throw new Error(`造孽的传参: ${args}`)

}

console.log(isSafe(COLOR_GREEN)) //true

console.log(isSafe(COLOR_RED)) //false

console.log(isSafe(LALALA)) //true

如果是Symbol,则符合我们的预期:

const COLOR_GREEN = Symbol(\公众1\"大众)//传参可以是字符串,数字,布尔或不填

const COLOR_RED = Symbol(\"大众2\"大众)

const LALALA = Symbol(\公众1\公众)

function isSafe(args) {

if (args === COLOR_RED) return false

if (args === COLOR_GREEN) return true

throw new Error(`造孽的传参: ${args}`)

}

console.log(isSafe(COLOR_GREEN)) //true

console.log(isSafe(COLOR_RED)) //false

console.log(COLOR_GREEN == LALALA) //false

console.log(isSafe(LALALA)) //throw error

把稳,Symbol不是一个布局器,不能new。
new Symbel(\"大众222\"大众)会抛错。

第二点,过往的工具属性都是字符串类型,如果我们没有用Object.defineProperty做处理,它们都能直接用for in遍历出来。
而Symbol属性不一样,遍历不出来,因此适用做工具的私有属性,由于你只有知道它的名字,才能访问到它。

var a = {

b: 11,

c: 22

}

var d = Symbol;

a[d] = 33

for(var i in a){

console.log(i, a[i]) //只有b,c

}

第三点,以往的数据类型都可以与字符串相加,变成一个字符串,或者减去一个数字,隐式转换为数字;而Symbol则直接抛错。

ar d = Symbol(\"大众11\"大众)

console.log(d - 1)

我们再来看它的静态方法:

Symbol.for

这类似一个Symbol, 但是它不表示独一无二的值,如果用Symbor.for创建了一个symbol, 下次再用相同的参数来访问,是返回相同的symbol。

Symbol.for(\"大众foo\"大众); // 创建一个 symbol 并放入 symbol 注册表中,键为 \"大众foo\"大众

Symbol.for(\公众foo\"大众); // 从 symbol 注册表中读取键为\"大众foo\"大众的 symbol

Symbol.for(\公众bar\"大众) === Symbol.for(\"大众bar\公众); // true,证明了上面说的

Symbol(\"大众bar\"大众) === Symbol(\"大众bar\公众); // false,Symbol 函数每次都会返回新的一个 symbol

var sym = Symbol.for(\"大众mario\"大众);

sym.toString;

上面例子是从火狐官方文档拿出来的,提到注册表这样的东西,换言之,我们所有由Symbol.for创建的symbol都由一个内部工具所管理。

Symbol.keyFor

Symbol.keyFor方法返回一个已注册的 symbol 类型值的key。
key便是我们的传参,也即是同于symbol的description属性。

let s1 = Symbol.for(\公众111\"大众);

console.log( Symbol.keyFor(s1) ) // \公众111\"大众

console.log(s1.description) // \"大众111\"大众

let s2 = Symbol(\"大众222\公众);

console.log( Symbol.keyFor(s2)) // undefined

console.log(s2.description) // \公众222\"大众

let s3 = Symbol.for(111);

console.log( Symbol.keyFor(s3) ) // \公众111\公众

console.log(s3.description) // \"大众111\公众

须要把稳的是,Symbol.for为 Symbol 值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。

iframe = document.createElement('iframe');

iframe.src = String(window.location);

document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for('111') === Symbol.for('111')// true

Symbol.iterator

在es6中添加了for of循环,相对付for in循环,它是直接遍历出值。
究其缘故原由,是由于数组原型上添加Symbol.iterator,它便是一个内置的迭代器,而for of便是实行函数的语法。
像数组,字符串,arguments, NodeList, TypeArray, Set, Map, WeakSet, WeatMap的原型都加上Symbol.iterator,因此都可以用for of循环。

console.log(Symbol.iterator in new String('sss')) // 将大略类型包装成工具才能利用in

console.log(Symbol.iterator in [1,2,3] )

console.log(Symbol.iterator in new Set(['a','b','c','a']))

for(var i of \公众123\公众){

console.log(i) //1,2 3

}

但我们对普通工具进行for of循环则碰着非常,须要我们自行添加。

Object.prototype[Symbol.iterator] = function {

var keys = Object.keys(this);

var index = 0;

return {

next: => {

var obj = {

value: this[keys[index]],

done: index+1 > keys.length

};

index++;

return obj;

}

};

};

var a = {

name:'ruby',

age:13,

home:\公众广东\公众

}

for (var val of a) {

console.log(val);

}

Symbol.asyncIterator

Symbol.asyncIterator与for await of循环一起利用,见上面异步一节。

Symbol.replace、search、split

这几个静态属性都与正则有关,我们会创造这个方法名在字符串也有相同的脸孔,它们便是改变这些方法的行为,让它们能吸收一个工具,这些工具有相应的symbol保护方法。
详细见下面例子:

class Search1 {

constructor(value) {

this.value = value;

}

[Symbol.search](string) {

return string.indexOf(this.value);

}

}

console.log('foobar'.search(new Search1('bar')));

class Replace1 {

constructor(value) {

this.value = value;

}

[Symbol.replace](string) {

return `s/${string}/${this.value}/g`;

}

}

console.log('foo'.replace(new Replace1('bar')));

class Split1 {

constructor(value) {

this.value = value;

}

[Symbol.split](string) {

var index = string.indexOf(this.value);

return this.value + string.substr(0, index) + \公众/\"大众

+ string.substr(index + this.value.length);

}

}

console.log('foobar'.split(new Split1('foo')));

Symbol.toStringTag

可以决定自定义类的 Object.prototype.toString.call的结果:

class ValidatorClass {

get [Symbol.toStringTag] {

return 'Validator';

}

}

console.log(Object.prototype.toString.call(new ValidatorClass));

// expected output: \公众[object Validator]\"大众

此外,还有许多静态属性, 方便我们对措辞的底层做更精细的制订,这里就不逐一罗列了。

我们再看BigInt, 它就没有这么繁芜。
早期JavaScript的整数范围是2的53次方减一的正负数,如果超过这范围,数值就不准确了。

console.log(1234567890123456789 123) //这显然不对

因此我们非常须要这样的数据类型,在它没有出来前只能利用字符串来仿照。
然后chrome67中,已经内置这种类型了。
想利用它,可能直接在数字后加一个n,或者利用BigInt创建它。

const theBiggestInt = 9007199254740991n;

const alsoHuge = BigInt(9007199254740991);

// ↪ 9007199254740991n

const hugeString = BigInt(\公众9007199254740991\公众);

// ↪ 9007199254740991n

const hugeHex = BigInt(\"大众0x1fffffffffffff\公众);

// ↪ 9007199254740991n

const hugeBin = BigInt(\公众0b11111111111111111111111111111111111111111111111111111\"大众);

console.log(typeof hugeBin) //bigint

反射机制的完善

反射机制指的是程序在运行时能够获取自身的信息。
例如一个工具能够在运行时知道自己哪些属性被实行了什么操作。

最先映入我们眼帘的是IE8带来的get, set关键字。
这便是其他措辞的setter, getter。
看似是一个属性,实在是两个方法。

var inner = 0;

var obj = {

set a(val){

console.log(\"大众set a \公众)

inner = val

},

get a{

console.log(\"大众get a \"大众)

return inner +2

}

}

console.log(obj)

obj.a = 111

console.log(obj.a) // 113

image_1dojfhdi1vqbdqg1hr4mkt52h9.png-11.9kB

但在babel.js还没有出身的年代,新语法是很难生存的,因此IE8又搞了两个类似的API,用来定义setter, getter:Object.defineProperty与Object.defineProperties。
后者是前者的强化版。

var inner = 0;

var obj = {}

Object.defineProperty(obj, 'a', {

set:function(val){

console.log(\公众set a \公众)

inner = val

},

get: function{

console.log(\公众get a \"大众)

return inner +2

}

})

console.log(obj)

obj.a = 111

console.log(obj.a) // 113

而标准浏览器怎么办?IE8时期,firefox一方也有相应的私有实现:__defineGetter____defineSetter__,它们是挂在工具的原型链上。

var inner = 0;

var obj = {}

obj.__defineSetter__(\"大众a\"大众, function(val){

console.log(\"大众set a \"大众)

inner = val

})

obj.__defineGetter__(\公众a\"大众, function{

console.log(\"大众get a \"大众)

return inner + 4

})

console.log(obj)

obj.a = 111

console.log(obj.a) // 115

在三大框架没有崛起之前,是MVVM的狂欢时期,avalon等框架便是利用这些方法实现了MVVM中的VM。

setter与getter是IE结束十多年瀦中添加的一个主要特性,让JavaScript变得当代化,也更加魔幻。

但它们只能监听工具属性的赋值取值,如果一个工具开始没有定义,后来添加就监听不到;我们删除一个工具属性也监听不到;我们对数组push进一个元素也监听不到,对某个类进行实例化也监听不到……总之,局b限还是很大的。
于是chrome某个版本添加了Object.observe,支持异步监听工具的各种举动(如\公众add\公众, \公众update\"大众, \"大众delete\公众, \"大众reconfigure\公众, \"大众setPrototype\"大众, \公众preventExtensions\公众),但是其他浏览器不支持,于是esma委员会又合计搞了另一个逆天的东西Proxy。

Proxy

这个是es6大名鼎鼎的魔术代理工具,与Object.defineProperty一样,无法以旧有方法来仿照它。

下面是它的用法,其拦截器所代表的操作:

let p = new Proxy({}, {//拦截工具,上面有如下拦截器

get: function(target, name){

// obj.aaa

},

set: function(target, name, value){

// obj.aaa = bbb

},

construct: function(target, args) {

//new

},

apply: function(target, thisArg, args) {

//实行某个方法

},

defineProperty: function (target, name, descriptor) {

// Object.defineProperty

},

deleteProperty: function (target, name) {

//delete

},

has: function (target, name) {

// in

},

ownKeys: function (target, name) {

// Object.getOwnPropertyNames

// Object.getOwnPropertySymbols

// Object.keys Reflect.ownKeys

},

isExtensible: function(target) {

// Object.isExtensible。

},

preventExtensions: function(target) {

// Object.preventExtensions

},

getOwnPropertyDescriptor: function(target, prop) {

// Object.getOwnPropertyDescriptor

},

getPrototypeOf: function(target){

// Object.getPrototypeOf,

// Reflect.getPrototypeOf,

// __proto__

// Object.prototype.isPrototypeOf与instanceof

},

setPrototypeOf: function(target, prototype) {

// Object.setPrototypeOf.

}

});

这个工具在vue3, mobx中被大量利用。

Reflect

Reflect与Proxy一同推出,Reflect上的方法与Proxy的拦截器同名,用于一些Object.xxx操作与in, new , delete等关键字的操作(这时只是将它们变成函数办法)。
换言之,Proxy是接活的,Reflect是干活的,火狐官网的示例也表示这一点。

var p = new Proxy({

a: 11

}, {

deleteProperty: function (target, name) {

console.log(arguments)

return Reflect.deleteProperty(target, name)

}

})

delete p.a

它们与Object.xxx最大的差异是,它们都有返回结果, 并且传参缺点不会报错(如Object.defineProperty)。
可能官方认为将这些元操作方法放到Object上有点欠妥,于是推出了Reflect。

Reflect统共有13个静态方法:

Reflect.apply(target, thisArg, args)

Reflect.construct(target, args)

Reflect.get(target, name, receiver)

Reflect.set(target, name, value, receiver)

Reflect.defineProperty(target, name, desc)

Reflect.deleteProperty(target, name)

Reflect.has(target, name)

Reflect.ownKeys(target)

Reflect.isExtensible(target)

Reflect.preventExtensions(target)

Reflect.getOwnPropertyDescriptor(target, name)

Reflect.getPrototypeOf(target)

Reflect.setPrototypeOf(target, prototype)

更顺手的语法糖

除了添加这些方法外,JavaScript底层的parser也大动手术,让它支持更多语法糖。
语法糖都可以写成对应的函数,但未便利。
总的来说,语法糖是想让大家的代码更加精简。

新近添加如下语法糖:

工具简写,参看类的组织形式

扩展运算符(),用于工具的浅拷贝

箭头函数,省略function关键字,与数学公式走近,能绑定this与略去return

for of(遍历可迭代工具的所有值, for in是遍历工具的键或索引)

数字格式化, 如1_222_333

字符串模板化与天然多行支持,如hello ${world}

幂运算符,

可选链,let x = foo?.bar.baz;

空值合并运算符, let x = foo ?? bar;

函数的默认参数

总结

ECMAScript正在快速发展,常常会有新特性被引入,有兴趣可以查询babel的语法插件(https://www.babeljs.cn/docs/plugins),理解更详细的用法。
相信有了这些新特色的支持,大家再也不敢看小JavaScript了。

作者简介:司徒正美,拥有十年纯前端履历,著有《JavaScript框架设计》一书,去哪儿网公共技能部前端架构师。
爱好开源,拥有mass、Avalon、nanachi等前端框架。
目前在主导公司的小程序、快运用的研发项目。

【END】