司徒正美:JavaScript 为什么能活到现在?
JavaScript能发展到现在的程度已经经历不少的坎坷,早产带来的某些缺陷是永久性的,因此浏览器才有禁用JavaScript的选项。甚至在jQuery时代有人问出这样的问题,jQuery与JavaScript哪个快?在Babel.js出来之前,发明一门全新的语言代码代替JavaScript的呼声一直不绝于耳,前有VBScript,Coffee, 后有Dartjs, WebAssembly。要不是它是所有浏览器都内置的脚本语言, 可能就命绝于此。浏览器就是它的那个有钱的丈母娘。此外源源不断的类库框架,则是它的武器库,从底层革新了它自己。为什么这么说呢?
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。基本上在字符串与数组这两大类别扩充,它们在日常业务中不断被使用,因此不断变重复造轮子,因此亟待官方化。
这些原型方法非常有用,以致于在面试中经常被问到,如果去除字符串两边的空白,如何扁平化一个数组?
在没有类的时代,每个流行框架都会带一个创建类的方法,可见大家都不太认同原型这种复用机制。
function Person(name) { this.name = name;}//定义一个方法并且赋值给构造函数的原型Person.prototype.sayName = function () { return this.name;};var p = new Person(ruby);console.log(p.sayName()) // rubyclass 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>
标准还会导致页面渲染堵塞,出现白屏现象。
<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)。
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.jsdefine(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 美刀赏金活动,让国内高手一展身手。
image_1doan2vfl17ld1nin1hbm182c9b9p.png-72.9kB
相对而言,nodejs模块系统就简单多了,它没有专门用于包裹用户代码的define方法,它不需要显式声明依赖。
//world.jsexports.world = function() { console.log(Hello World);}//main.jslet 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.mjsimport * as test from ./test;//aaa.js或aaa.mjsimport {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();// 在现在发送请求ajax({ url: url, data: {}, success:function(res){ // 在未来某个时刻执行 B(res) }})C();//执行顺序:A -> C -> B
回调函数是主函数的后继方法,基本上能保证,主函数执行后,它能在之后某个时刻被执行一次。但随着功能的细分,在微信小程序或快应用中,它们拆分成三个,即一个方法跟着三个回调。
// https://doc.quickapp.cn/features/system/share.htmlimport share from @system.shareshare.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最初叫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 处理错误
process.on(unhandledRejection, (reason, promise) => { console.log(未处理的拒绝:, promise, 原因:, reason); // 记录日志、抛出错误、或其他逻辑。});
除此之外,esma2020年还为Promise添加了三个静态方法:Promise.all()和Promise.race(),Promise.allSettled() 。
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库,就是基于上述原理摆平异步问题。有兴趣的同学可以下来看看。
上节章的生成器已经完美地解决异步的逻辑以同步的代码编写 的问题了,什么异常,可以直接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
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 definedconsole.log(b); // Uncaught ReferenceError: b is not definedlet a = 1; //Uncaught SyntaxError: Identifier a has already been declaredconst b = 2;//不存在变量提升,因此块级作用域外层无法访问if(true){ var bar = "bar"; let baz = "baz"; const qux = "qux";}console.log(bar);//barconsole.log(baz);//baz is not definedconsole.log(qux);//qux is not defined
const声明则比let声明多了一个功能,就让目标变量的值不能再次改变,即其他语言的常量。
在javascript, 我们通过typeof与Object.prototype.toString.call可以区分出对象的类型,过去总有7种类型:undefined, null, string, number, boolean, function, object。现在又多出两个类型,一个是es6引进的Symbol,另一个是es2019的BigInt。
console.log(typeof 9007199254740991n); // "bigint"console.log(typeof Symbol("aaa")); // "symbol"
Symbol拥有三个特性,创建的值是独一无二的,附加在对象是不可遍历的,不支持隐式转换。此外Symbol上面还有其他静态方法,用来为对象扩展更多功能。
我们先看它如何表示独一无二的属性值。如果没有Symbol,我们寻常表示常量的方法是不可靠的。
const COLOR_GREEN = 1const COLOR_RED = 2const 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)) //trueconsole.log(isSafe(COLOR_RED)) //falseconsole.log(isSafe(LALALA)) //true
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)) //trueconsole.log(isSafe(COLOR_RED)) //falseconsole.log(COLOR_GREEN == LALALA) //falseconsole.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] = 33for(var i in a){ console.log(i, a[i]) //只有b,c}
第三点,以往的数据类型都可以与字符串相加,变成一个字符串,或者减去一个数字,隐式转换为数字;而Symbol则直接抛错。
ar d = Symbol("11")console.log(d - 1)
这类似一个Symbol(), 但是它不表示独一无二的值,如果用Symbor.for创建了一个symbol, 下次再用相同的参数来访问,是返回相同的symbol。
Symbol.for("foo"); // 创建一个 symbol 并放入 symbol 注册表中,键为 "foo"Symbol.for("foo"); // 从 symbol 注册表中读取键为"foo"的 symbolSymbol.for("bar") === Symbol.for("bar"); // true,证明了上面说的Symbol("bar") === Symbol("bar"); // false,Symbol() 函数每次都会返回新的一个 symbolvar sym = Symbol.for("mario");sym.toString();
上面例子是从火狐官方文档拿出来的,提到注册表这样的东西,换言之,我们所有由Symbol.for创建的symbol都由一个内部对象所管理。
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)) // undefinedconsole.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
在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)) // 将简单类型包装成对象才能使用inconsole.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与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)));
可以决定自定义类的 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);// ↪ 9007199254740991nconst hugeString = BigInt("9007199254740991");// ↪ 9007199254740991nconst hugeHex = BigInt("0x1fffffffffffff");// ↪ 9007199254740991nconst 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 = 111console.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 = 111console.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 = 111console.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。
这个是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(). }});
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.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等前端框架。目前在主导公司的小程序、快应用的研发项目。
THE END