js代码-js代码外包-北京js代码外包

搜 索

Search:js代码


js HTML DOM 属性js代码

HTML DOM 属性 属性是节点(HTML 元素)的值,您能够获取或设置。 编程接口 可通过 JavaScript (以及其他编程语言)对 HTML DOM 进行访问。 所有 HTML 元素HTML DOM 属性属性是节点(HTML 元素)的值,您能够获取或设置。编程接口可通过 JavaScript (以及其他编程语言)对 HTML DOM 进行访问。所有 HTML 元素被定义为对象,而编程接口则是对象方法和对象属性。方法是您能够执行的动作(比如添加或修改元素)。属性是您能够获取或设置的值(比如节点的名称或内容)。innerHTML 属性获取元素内容的最简单方法是使用 innerHTML 属性。innerHTML 属性对于获取或替换 HTML 元素的内容很有用。实例下面的代码获取 id="intro" 的 <p> 元素的 innerHTML:实例<html><body><p id="intro">Hello World!</p><script>var txt=document.getElementById("intro").innerHTML;document.write(txt);</script></body></html>在上面的例子中,getElementById 是一个方法,而 innerHTML 是属性。innerHTML 属性可用于获取或改变任意 HTML 元素,包括 <html> 和 <body>。nodeName 属性nodeName 属性规定节点的名称。nodeName 是只读的元素节点的 nodeName 与标签名相同属性节点的 nodeName 与属性名相同文本节点的 nodeName 始终是 #text文档节点的 nodeName 始终是 #document注释:nodeName 始终包含 HTML 元素的大写字母标签名。nodeValue 属性nodeValue 属性规定节点的值。元素节点的 nodeValue 是 undefined 或 null文本节点的 nodeValue 是文本本身属性节点的 nodeValue 是属性值获取元素的值下面的例子会取回 <p id="intro"> 标签的文本节点值:实例<html><body><p id="intro">Hello World!</p><script type="text/javascript">x=document.getElementById("intro");document.write(x.firstChild.nodeValue);</script></body></html>nodeType 属性nodeType 属性返回节点的类型。nodeType 是只读的。比较重要的节点类型有:元素类型NodeType元素1属性2文本3注释8文档9
标签:

js HTML DOM - 修改js代码

HTML DOM - 修改 修改 HTML = 改变元素、属性、样式和事件。 修改 HTML 元素 修改 HTML DOM 意味着许多不同的方面:改变 HTML 内容 改变 CSS 样HTML DOM - 修改修改 HTML = 改变元素、属性、样式和事件。修改 HTML 元素修改 HTML DOM 意味着许多不同的方面:改变 HTML 内容改变 CSS 样式改变 HTML 属性创建新的 HTML 元素删除已有的 HTML 元素改变事件(处理程序)在接下来的章节,我们会深入学习修改 HTML DOM 的常用方法。创建 HTML 内容改变元素内容的最简答的方法是使用 innerHTML 属性。下面的例子改变一个 <p> 元素的 HTML 内容:实例<html><body><p id="p1">Hello World!</p><script>document.getElementById("p1").innerHTML="New text!";</script></body></html>提示:我们将在下面的章节为您解释例子中的细节。改变 HTML 样式通过 HTML DOM,您能够访问 HTML 元素的样式对象。下面的例子改变一个段落的 HTML 样式:实例<html><body><p id="p2">Hello world!</p><script>document.getElementById("p2").style.color="blue";</script></body></html>创建新的 HTML 元素如需向 HTML DOM 添加新元素,您首先必须创建该元素(元素节点),然后把它追加到已有的元素上。实例<div id="d1"><p id="p1">This is a paragraph.</p><p id="p2">This is another paragraph.</p></div><script>var para=document.createElement("p");var node=document.createTextNode("This is new.");para.appendChild(node);var element=document.getElementById("d1");element.appendChild(para);</script>
标签:

js代码 es6函数的扩展,js函数的扩展

函数的扩展 函数参数的默认值 基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。 function log(x, y) {y = y || 'World';函数的扩展函数参数的默认值基本用法在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。function log(x, y) {y = y || 'World';console.log(x, y);}log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', '') // Hello World上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。if (typeof y === 'undefined') {y = 'World';}ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。function log(x, y = 'World') {console.log(x, y);}log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', '') // Hello可以看到,ES6的写法比ES5简洁许多,而且非常自然。下面是另一个例子。function Point(x = 0, y = 0) {this.x = x;this.y = y;}var p = new Point();p // { x: 0, y: 0 }除了简洁,ES6的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。参数变量是默认声明的,所以不能用let或const再次声明。function foo(x = 5) {let x = 1; // errorconst x = 2; // error}上面代码中,参数变量x是默认声明的,在函数体中,不能用let或const再次声明,否则会报错。与解构赋值默认值结合使用参数默认值可以与解构赋值的默认值,结合起来使用。function foo({x, y = 5}) {console.log(x, y);}foo({}) // undefined, 5foo({x: 1}) // 1, 5foo({x: 1, y: 2}) // 1, 2foo() // TypeError: Cannot read property 'x' of undefined上面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值而生成。如果函数foo调用时参数不是对象,变量x和y就不会生成,从而报错。如果参数对象没有y属性,y的默认值5才会生效。下面是另一个对象的解构赋值默认值的例子。function fetch(url, { body = '', method = 'GET', headers = {} }) {console.log(method);}fetch('http://example.com', {})// "GET"fetch('http://example.com')// 报错上面代码中,如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。function fetch(url, { method = 'GET' } = {}) {console.log(method);}fetch('http://example.com')// "GET"上面代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET。再请问下面两种写法有什么差别?// 写法一function m1({x = 0, y = 0} = {}) {return [x, y];}// 写法二function m2({x, y} = { x: 0, y: 0 }) {return [x, y];}上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。// 函数没有参数的情况m1() // [0, 0]m2() // [0, 0]// x和y都有值的情况m1({x: 3, y: 8}) // [3, 8]m2({x: 3, y: 8}) // [3, 8]// x有值,y无值的情况m1({x: 3}) // [3, 0]m2({x: 3}) // [3, undefined]// x和y都无值的情况m1({}) // [0, 0];m2({}) // [undefined, undefined]m1({z: 3}) // [0, 0]m2({z: 3}) // [undefined, undefined]参数默认值的位置通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。// 例一function f(x = 1, y) {return [x, y];}f() // [1, undefined]f(2) // [2, undefined])f(, 1) // 报错f(undefined, 1) // [1, 1]// 例二function f(x, y = 5, z) {return [x, y, z];}f() // [undefined, 5, undefined]f(1) // [1, 5, undefined]f(1, ,2) // 报错f(1, undefined, 2) // [1, 5, 2]上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。如果传入undefined,将触发该参数等于默认值,null则没有这个效果。function foo(x = 5, y = 6) {console.log(x, y);}foo(undefined, null)// 5 null上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。函数的length属性指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。(function (a) {}).length // 1(function (a = 5) {}).length // 0(function (a, b, c = 5) {}).length // 2上面代码中,length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了3个参数,其中有一个参数c指定了默认值,因此length属性等于3减去1,最后得到2。这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入length属性。(function(...args) {}).length // 0如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。(function (a = 0, b, c) {}).length // 0(function (a, b = 1, c) {}).length // 1作用域一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域。var x = 1;function f(x, y = x) {console.log(y);}f(2) // 2上面代码中,参数y的默认值等于x。调用时,由于函数作用域内部的变量x已经生成,所以y等于参数x,而不是全局变量x。如果调用时,函数作用域内部的变量x没有生成,结果就会不一样。let x = 1;function f(y = x) {let x = 2;console.log(y);}f() // 1上面代码中,函数调用时,y的默认值变量x尚未在函数内部生成,所以x指向全局变量。如果此时,全局变量x不存在,就会报错。function f(y = x) {let x = 2;console.log(y);}f() // ReferenceError: x is not defined下面这样写,也会报错。var x = 1;function foo(x = x) {// ...}foo() // ReferenceError: x is not defined上面代码中,函数foo的参数x的默认值也是x。这时,默认值x的作用域是函数作用域,而不是全局作用域。由于在函数作用域中,存在变量x,但是默认值在x赋值之前先执行了,所以这时属于暂时性死区(参见《let和const命令》一章),任何对x的操作都会报错。如果参数的默认值是一个函数,该函数的作用域是其声明时所在的作用域。请看下面的例子。let foo = 'outer';function bar(func = x => foo) {let foo = 'inner';console.log(func()); // outer}bar();上面代码中,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。这个匿名函数声明时,bar函数的作用域还没有形成,所以匿名函数里面的foo指向外层作用域的foo,输出outer。如果写成下面这样,就会报错。function bar(func = () => foo) {let foo = 'inner';console.log(func());}bar() // ReferenceError: foo is not defined上面代码中,匿名函数里面的foo指向函数外层,但是函数外层并没有声明foo,所以就报错了。下面是一个更复杂的例子。var x = 1;function foo(x, y = function() { x = 2; }) {var x = 3;y();console.log(x);}foo() // 3上面代码中,函数foo的参数y的默认值是一个匿名函数。函数foo调用时,它的参数x的值为undefined,所以y函数内部的x一开始是undefined,后来被重新赋值2。但是,函数foo内部重新声明了一个x,值为3,这两个x是不一样的,互相不产生影响,因此最后输出3。如果将var x = 3的var去除,两个x就是一样的,最后输出的就是2。var x = 1;function foo(x, y = function() { x = 2; }) {x = 3;y();console.log(x);}foo() // 2应用利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。function throwIfMissing() {throw new Error('Missing parameter');}function foo(mustBeProvided = throwIfMissing()) {return mustBeProvided;}foo()// Error: Missing parameter上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。从上面代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(即函数名之后有一对圆括号),这&#x
标签:

js代码 es6二进制数组,js二进制数组

ES6 二进制数组 二进制数组(ArrayBuffer对象、TypedArray视图和DataView视图)是JavaScript操作二进制数据的一个接口。这些对象早就存在,属于独立的规格(2ES6 二进制数组二进制数组(ArrayBuffer对象、TypedArray视图和DataView视图)是JavaScript操作二进制数据的一个接口。这些对象早就存在,属于独立的规格(2021年2月发布),ES6将它们纳入了ECMAScript规格,并且增加了新的方法。这个接口的原始设计目的,与WebGL项目有关。所谓WebGL,就是指浏览器与显卡之间的通信接口,为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个32位整数,两端的JavaScript脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像C语言那样,直接操作字节,将4个字节的32位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。二进制数组就是在这种背景下诞生的。它很像C语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了JavaScript处理二进制数据的能力,使得开发者有可能通过JavaScript与操作系统的原生接口进行二进制通信。二进制数组由三类对象组成。(1)ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。(2)TypedArray视图:共包括9种类型的视图,比如Uint8Array(无符号8位整数)数组视图, Int16Array(16位整数)数组视图, Float32Array(32位浮点数)数组视图等等。(3)DataView视图:可以自定义复合格式的视图,比如第一个字节是Uint8(无符号8位整数)、第二、三个字节是Int16(16位整数)、第四个字节开始是Float32(32位浮点数)等等,此外还可以自定义字节序。简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。TypedArray视图支持的数据类型一共有9种(DataView视图支持除Uint8C以外的其他8种)。数据类型字节长度含义对应的C语言类型Int818位带符号整数signed charUint818位不带符号整数unsigned charUint8C18位不带符号整数(自动过滤溢出)unsigned charInt16216位带符号整数shortUint16216位不带符号整数unsigned shortInt32432位带符号整数intUint32432位不带符号的整数unsigned intFloat32432位浮点数floatFloat64864位浮点数double注意,二进制数组并不是真正的数组,而是类似数组的对象。很多浏览器操作的API,用到了二进制数组操作二进制数据,下面是其中的几个。File APIXMLHttpRequestFetch APICanvasWebSocketsArrayBuffer对象概述ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。var buf = new ArrayBuffer(32);上面代码生成了一段32字节的内存区域,每个字节的值默认都是0。可以看到,ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。var buf = new ArrayBuffer(32);var dataView = new DataView(buf);dataView.getUint8(0) // 0上面代码对一段32字节的内存,建立DataView视图,然后以不带符号的8位整数格式,读取第一个元素,结果得到0,因为原始内存的ArrayBuffer对象,默认所有位都是0。另一种TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。var buffer = new ArrayBuffer(12);var x1 = new Int32Array(buffer);x1[0] = 1;var x2 = new Uint8Array(buffer);x2[0]= 2;x1[0] // 2上面代码对同一段内存,分别建立两种视图:32位带符号整数(Int32Array构造函数)和8位不带符号整数(Uint8Array构造函数)。由于两个视图对应的是同一段内存,一个视图修改底层内存,会影响到另一个视图。TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。var typedArray = new Uint8Array([0,1,2]);typedArray.length // 3typedArray[0] = 5;typedArray // [5, 1, 2]上面代码使用TypedArray视图的Uint8Array构造函数,新建一个不带符号的8位整数视图。可以看到,Uint8Array直接使用普通数组作为参数,对底层内存的赋值同时完成。ArrayBuffer.prototype.byteLengthArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。var buffer = new ArrayBuffer(32);buffer.byteLength// 32如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。if (buffer.byteLength === n) {// 成功} else {// 失败}ArrayBuffer.prototype.slice()ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。var buffer = new ArrayBuffer(8);var newBuffer = buffer.slice(0, 3);上面代码拷贝buffer对象的前3个字节(从0开始,到第3个字节前面结束),生成一个新的ArrayBuffer对象。slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。slice方法接受两个参数,第一个参数表示拷贝开始的字节序号(含该字节),第二个参数表示拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。ArrayBuffer.isView()ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。var buffer = new ArrayBuffer(8);ArrayBuffer.isView(buffer) // falsevar v = new Int32Array(buffer);ArrayBuffer.isView(v) // trueTypedArray视图概述ArrayBuffer对象作为内存区域,可以存放多种类型的数据。同一段内存,不同数据有不同的解读方式,这就叫做“视图”(view)。ArrayBuffer有两种视图,一种是TypedArray视图,另一种是DataView视图。前者的数组成员都是同一个数据类型,后者的数组成员可以是不同的数据类型。目前,TypedArray视图一共包括9种类型,每一种视图都是一种构造函数。Int8Array:8位有符号整数,长度1个字节。Uint8Array:8位无符号整数,长度1个字节。Uint8ClampedArray:8位无符号整数,长度1个字节,溢出处理不同。Int16Array:16位有符号整数,长度2个字节。Uint16Array:16位无符号整数,长度2个字节。Int32Array:32位有符号整数,长度4个字节。Uint32Array:32位无符号整数,长度4个字节。Float32Array:32位浮点数,长度4个字节。Float64Array:64位浮点数,长度8个字节。这9个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,都能用方括号运算符([])获取单个元素,所有数组的方法,在它们上面都能使用。普通数组与TypedArray数组的差异主要在以下方面。TypedArray数组的所有成员,都是同一种类型。TypedArray数组的成员是连续的,不会有空位。TypedArray数组成员的默认值为0。比如,new Array(10)返回一个普通数组,里面没有任何成员,只是10个空位;new Uint8Array(10)返回一个TypedArray数组,里面10个成员都是0。TypedArray数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。构造函数TypedArray数组提供9种构造函数,用来生成相应类型的数组实例。构造函数有多种用法。(1)TypedArray(buffer, byteOffset=0, length?)同一个ArrayBuffer对象之上,可以根据不同的数据类型,建立多个视图。// 创建一个8字节的ArrayBuffervar b = new ArrayBuffer(8);// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾var v1 = new Int32Array(b);// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾var v2 = new Uint8Array(b, 2);// 创建一个指向b的Int16视图,开始于字节2,长度为2var v3 = new Int16Array(b, 2, 2);上面代码在一段长度为8个字节的内存(b)之上,生成了三个视图:v1、v2和v3。视图的构造函数可以接受三个参数:第一个参数(必需):视图对应的底层ArrayBuffer对象。第二个参数(可选):视图开始的字节序号,默认从0开始。第三个参数(可选):视图包含的数据个数,默认直到本段内存区域结束。因此,v1、v2和v3是重叠的:v1[0]是一个32位整数,指向字节0~字节3;v2[0]是一个8位无符号整数,指向字节2;v3[0]是一个16位整数,指向字节2~字节3。只要任何一个视图对内存有所修改,就会在另外两个视图上反应出来。注意,byteOffset必须与所要建立的数据类型一致,否则会报错。var buffer = new ArrayBuffer(8);var i16 = new Int16Array(buffer, 1);// Uncaught RangeError: start offset of Int16Array should be a multiple of 2上面代码中,新生成一个8个字节的ArrayBuffer对象,然后在这个对象的第一个字节,建立带符号的16位整数视图,结果报错。因为,带符号的16位整数需要两个字节,所以byteOffset参数必须能够被2整除。如果想从任意字节开始解读ArrayBuffer对象,必须使用DataView视图,因为TypedArray视图只提供9种固定的解读格式。(2)TypedArray(length)视图还可以不通过ArrayBuffer对象,直接分配内存而生成。var f64a = new Float64Array(8);f64a[0] = 10;f64a[1] = 20;f64a[2] = f64a[0] + f64a[1];上面代码生成一个8个成员的Float64Array数组(共64字节),然后依次对每个成员赋值。这时,视图构造函数的参数就是成员的个数。可以看到,视图数组的赋值操作与普通数组的操作毫无两样。(3)TypedArray(typedArray)TypedArray数组的构造函数,可以接受另一个TypedArray实例作为参数。var typedArray = new Int8Array(new Uint8Array(4));上面代码中,Int8Array构造函数接受一个Uint8Array实例作为参数。注意,此时生成的新数&#
标签:

js代码 es6Class,jsClass

ES6 class Class基本语法 概述 JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子。 function Point(x, y) {thisES6 classClass基本语法概述JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子。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);上面这种写法跟传统的面向对象语言(比如C++和Java)差异很大,很容易让新学习这门语言的程序员感到困惑。ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的“类”改写,就是下面这样。//定义类class Point {constructor(x, y) {this.x = x;this.y = y;}toString() {return '(' + this.x + ', ' + this.y + ')';}}上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。ES6的类,完全可以看作构造函数的另一种写法。class Point {// ...}typeof Point // "function"Point === Point.prototype.constructor // true上面代码表明,类的数据类型就是函数,类本身就指向构造函数。使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。class Bar {doStuff() {console.log('stuff');}}var b = new Bar();b.doStuff() // "stuff"构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。class Point {constructor(){// ...}toString(){// ...}toValue(){// ...}}// 等同于Point.prototype = {toString(){},toValue(){}};在类的实例上面调用方法,其实就是调用原型上的方法。class B {}let b = new B();b.constructor === B.prototype.constructor // true上面代码中,b是B类的实例,它的constructor方法就是B类原型的constructor方法。由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。class Point {constructor(){// ...}}Object.assign(Point.prototype, {toString(){},toValue(){}});prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的。Point.prototype.constructor === Point // true另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。class Point {constructor(x, y) {// ...}toString() {// ...}}Object.keys(Point.prototype)// []Object.getOwnPropertyNames(Point.prototype)// ["constructor","toString"]上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与ES5的行为不一致。var Point = function (x, y) {// ...};Point.prototype.toString = function() {// ...};Object.keys(Point.prototype)// ["toString"]Object.getOwnPropertyNames(Point.prototype)// ["constructor","toString"]上面代码采用ES5的写法,toString方法就是可枚举的。类的属性名,可以采用表达式。let methodName = "getArea";class Square{constructor(length) {// ...}[methodName]() {// ...}}上面代码中,Square类的方法名getArea,是从表达式得到的。constructor方法constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。constructor() {}constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。class Foo {constructor() {return Object.create(null);}}new Foo() instanceof Foo// false上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。类的构造函数,不使用new是没法调用的,会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。class Foo {constructor() {return Object.create(null);}}Foo()// TypeError: Class constructor Foo cannot be invoked without 'new'类的实例对象生成类的实例对象的写法,与ES5完全一样,也是使用new命令。如果忘记加上new,像函数那样调用Class,将会报错。// 报错var point = Point(2, 3);// 正确var point = new Point(2, 3);与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。//定义类class Point {constructor(x, y) {this.x = x;this.y = y;}toString() {return '(' + this.x + ', ' + this.y + ')';}}var point = new Point(2, 3);point.toString() // (2, 3)point.hasOwnProperty('x') // truepoint.hasOwnProperty('y') // truepoint.hasOwnProperty('toString') // falsepoint.__proto__.hasOwnProperty('toString') // true上面代码中,x和y都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。与ES5一样,类的所有实例共享一个原型对象。var p1 = new Point(2,3);var p2 = new Point(3,2);p1.__proto__ === p2.__proto__//true上面代码中,p1和p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的。这也意味着,可以通过实例的__proto__属性为Class添加方法。var p1 = new Point(2,3);var p2 = new Point(3,2);p1.__proto__.printName = function () { return 'Oops' };p1.printName() // "Oops"p2.printName() // "Oops"var p3 = new Point(4,2);p3.printName() // "Oops"上面代码在p1的原型上添加了一个printName方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变Class的原始定义,影响到所有实例。不存在变量提升Class不存在变量提升(hoist),这一点与ES5完全不同。new Foo(); // ReferenceErrorclass Foo {}上面代码中,Foo类使用在前,定义在后,这样会报错,因为ES6不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。{let Foo = class {};class Bar extends Foo {}}上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。Class表达式与函数一样,类也可以使用表达式的形式定义。const MyClass = class Me {getClassName() {return Me.name;}};上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。let inst = new MyClass();inst.getClassName() // MeMe.name // ReferenceError: Me is not defined上面代码表示,Me只在Class内部有定义。如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。const MyClass = class { /* ... */ };采用Class表达式,可以写出立即执行的Class。let person = new class {constructor(name) {this.name = name;}sayName() {console.log(this.name);}}('张三');person.sayName(); // "张三"上面代码中,person是一个立即执行的类的实例。私有方法私有方法是常见需求,但ES6不提供,只能通过变通方法模拟实现。一种做法是在命名上加以区别。class Widget {// 公有方法foo (baz) {this._bar(baz);}// 私有方法_bar(baz) {return this.snaf = baz;}// ...}上面代码中,_bar方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。class Widget {foo (baz) {bar.call(this, baz);}// ...}function bar(baz) {return this.snaf = baz;}上面代码中,foo是公有方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。const bar = Symbol('bar');const snaf = Symbol('snaf');export default class myClass{// 公有方法foo(baz) {this[bar](baz);}// 私有方法[bar](baz) {return this[snaf] = baz;}// ...};上面代码中,bar和snaf都是Symbol值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果。this的指向类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。class Logger {printName(name =

js es6Proxy和Reflectjs代码

Proxy 和 Reflect Proxy 概述 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行Proxy 和 ReflectProxy 概述Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。var obj = new Proxy({}, {get: function (target, key, receiver) {console.log(`getting ${key}!`);return Reflect.get(target, key, receiver);},set: function (target, key, value, receiver) {console.log(`setting ${key}!`);return Reflect.set(target, key, value, receiver);}});上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj,去读写它的属性,就会得到下面的结果。obj.count = 1//setting count!++obj.count//getting count!//setting count!//2上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。var proxy = new Proxy(target, handler);Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。下面是另一个拦截读取属性行为的例子。var proxy = new Proxy({}, {get: function(target, property) {return 35;}});proxy.time // 35proxy.name // 35proxy.title // 35上面代码中,作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35。注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。如果handler没有设置任何拦截,那就等同于直接通向原对象。var target = {};var handler = {};var proxy = new Proxy(target, handler);proxy.a = 'b';target.a // "b"上面代码中,handler是一个空对象,没有任何拦截效果,访问handler就等同于访问target。一个技巧是将Proxy对象,设置到object.proxy属性,从而可以在object对象上调用。var object = { proxy: new Proxy(target, handler) };Proxy 实例也可以作为其他对象的原型对象。var proxy = new Proxy({}, {get: function(target, property) {return 35;}});let obj = Object.create(proxy);obj.time // 35上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。同一个拦截器函数,可以设置拦截多个操作。var handler = {get: function(target, name) {if (name === 'prototype') {return Object.prototype;}return 'Hello, ' + name;},apply: function(target, thisBinding, args) {return args[0];},construct: function(target, args) {return {value: args[1]};}};var fproxy = new Proxy(function(x, y) {return x + y;}, handler);fproxy(1, 2) // 1new fproxy(1,2) // {value: 2}fproxy.prototype === Object.prototype // truefproxy.foo // "Hello, foo"下面是 Proxy 支持的拦截操作一览。对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。(1)get(target, propKey, receiver)拦截对象属性的读取,比如proxy.foo和proxy['foo']。最后一个参数receiver是一个对象,可选,参见下面Reflect.get的部分。(2)set(target, propKey, value, receiver)拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。(3)has(target, propKey)拦截propKey in proxy的操作,以及对象的hasOwnProperty方法,返回一个布尔值。(4)deleteProperty(target, propKey)拦截delete proxy[propKey]的操作,返回一个布尔值。(5)ownKeys(target)拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该方法返回对象所有自身的属性,而Object.keys()仅返回对象可遍历的属性。(6)getOwnPropertyDescriptor(target, propKey)拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。(7)defineProperty(target, propKey, propDesc)拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。(8)preventExtensions(target)拦截Object.preventExtensions(proxy),返回一个布尔值。(9)getPrototypeOf(target)拦截Object.getPrototypeOf(proxy),返回一个对象。(10)isExtensible(target)拦截Object.isExtensible(proxy),返回一个布尔值。(11)setPrototypeOf(target, proto)拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。(12)apply(target, object, args)拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。(13)construct(target, args)拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。Proxy实例的方法下面是上面这些拦截方法的详细介绍。get()get方法用于拦截某个属性的读取操作。上文已经有一个例子,下面是另一个拦截读取操作的例子。var person = {name: "张三"};var proxy = new Proxy(person, {get: function(target, property) {if (property in target) {return target[property];} else {throw new ReferenceError("Property \"" + property + "\" does not exist.");}}});proxy.name // "张三"proxy.age // 抛出一个错误上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined。get方法可以继承。let proto = new Proxy({}, {get(target, propertyKey, receiver) {console.log('GET '+propertyKey);return target[propertyKey];}});let obj = Object.create(proto);obj.xxx // "GET xxx"上面代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时,拦截会生效。下面的例子使用get拦截,实现数组读取负数的索引。function createArray(...elements) {let handler = {get(target, propKey, receiver) {let index = Number(propKey);if (index < 0) {propKey = String(target.length + index);}return Reflect.get(target, propKey, receiver);}};let target = [];target.push(...elements);return new Proxy(target, handler);}let arr = createArray('a', 'b', 'c');arr[-1] // c上面代码中,数组的位置参数是-1,就会输出数组的倒数最后一个成员。利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。var pipe = (function () {return function (value) {var funcStack = [];var oproxy = new Proxy({} , {get : function (pipeObject, fnName) {if (fnName === 'get') {return funcStack.reduce(function (val, fn) {return fn(val);},value);}funcStack.push(window[fnName]);return oproxy;}});return oproxy;}}());var double = n => n * 2;var pow= n => n * n;var reverseInt = n => n.toString().split("").reverse().join("") | 0;pipe(3).double.pow.reverseInt.get; // 63上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。下面的例子则是利用get拦截,实现一个生成各种DOM节点的通用函数dom。const dom = new Proxy({}, {get(target, property) {return function(attrs = {}, ...children) {const el = document.createElement(property);for (let prop of Object.keys(attrs)) {el.setAttribute(prop, attrs[prop]);}for (let child of children) {if (typeof child === 'string') {child = document.createTextNode(child);}el.appendChild(child);}return el;}}});const el = dom.div({},'Hello, my name is ',dom.a({href: '//example.com'}, 'Mark'),'. I like:',dom.ul({},dom.li({}, 'The web'),dom.li({}, 'Food'),dom.li({}, '…actually that\'s it')));document.body.appendChild(el);set()set方法用来拦截某个属性的赋值操作。假定Person对象有一个age属性,该属性应该是一个不大于200的整数,那么可以使用Proxy保证age的属性值符合要求。let validator = {set: function(obj, prop, value) {if (prop === 'age') {if (!Number.isInteger(value)) {throw new TypeError('The age is not an integer');}if (value > 200) {throw new RangeError('The age seems invalid');}}// 对于age以外的属性,直接保存obj[prop] = value;}};let person = new Proxy({}, validator);person.age = 100;person.age // 100person.age = 'young' // 报错person.age = 300 // 报错上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误。利用set方法,还可以数据绑定,即每当对象发生变化时,会自动更新DOM。有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合get和set方法,就可以做到防止这些内部属性被外部读写。var handler = {get (target, key) {invariant(key, 'get');return target[key];},set (target, key, value) {invariant(key, 'set');target[key] = value;return true;}};function invariant (key, action) {if (key[0] === '_') {throw new Error(`Invalid attempt to ${action} private "${key}" property`);}}var target = {};var proxy = new Proxy(target, handler);proxy._prop// Error: Invalid attempt to get private "_prop" propertyproxy._prop = 'c'// Error: Invalid attempt to set private "_prop" property上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。apply()apply方法拦截函数的调用、call和apply操作。

js es6Promise对象js代码

ES6 promise对象 Promise的含义 Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准ES6 promise对象Promise的含义Promise是异步编程的一种解决方案,比传统的解决方案‡‡回调函数和事件‡‡更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。Promise对象有以下两个特点。(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。如果某些事件不断地反复发生,一般来说,使用stream模式是比部署Promise更好的选择。基本用法ES6规定,Promise对象是一个构造函数,用来生成Promise实例。下面代码创造了一个Promise实例。var promise = new Promise(function(resolve, reject) {// ... some codeif (/* 异步操作成功 */){resolve(value);} else {reject(error);}});Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。promise.then(function(value) {// success}, function(error) {// failure});then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。下面是一个Promise对象的简单例子。function timeout(ms) {return new Promise((resolve, reject) => {setTimeout(resolve, ms, 'done');});}timeout(100).then((value) => {console.log(value);});上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为Resolved,就会触发then方法绑定的回调函数。Promise新建后就会立即执行。let promise = new Promise(function(resolve, reject) {console.log('Promise');resolve();});promise.then(function() {console.log('Resolved.');});console.log('Hi!');// Promise// Hi!// Resolved上面代码中,Promise新建后立即执行,所以首先输出的是“Promise”。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“Resolved”最后输出。下面是异步加载图片的例子。function loadImageAsync(url) {return new Promise(function(resolve, reject) {var image = new Image();image.onload = function() {resolve(image);};image.onerror = function() {reject(new Error('Could not load image at ' + url));};image.src = url;});}上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。下面是一个用Promise对象实现的Ajax操作的例子。var getJSON = function(url) {var promise = new Promise(function(resolve, reject){var client = new XMLHttpRequest();client.open("GET", url);client.onreadystatechange = handler;client.responseType = "json";client.setRequestHeader("Accept", "application/json");client.send();function handler() {if (this.readyState !== 4) {return;}if (this.status === 200) {resolve(this.response);} else {reject(new Error(this.statusText));}};});return promise;};getJSON("/posts.json").then(function(json) {console.log('Contents: ' + json);}, function(error) {console.error('出错了', error);});上面代码中,getJSON是对XMLHttpRequest对象的封装,用于发出一个针对JSON数据的HTTP请求,并且返回一个Promise对象。需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,比如像下面这样。var p1 = new Promise(function (resolve, reject) {// ...});var p2 = new Promise(function (resolve, reject) {// ...resolve(p1);})上面代码中,p1和p2都是Promise的实例,但是p2的resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是Pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调函数将会立刻执行。var p1 = new Promise(function (resolve, reject) {setTimeout(() => reject(new Error('fail')), 3000)})var p2 = new Promise(function (resolve, reject) {setTimeout(() => resolve(p1), 1000)})p2.then(result => console.log(result)).catch(error => console.log(error))// Error: fail上面代码中,p1是一个Promise,3秒之后变为rejected。p2的状态在1秒之后改变,resolve方法返回的是p1。此时,由于p2返回的是另一个Promise,所以后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数。Promise.prototype.then()Promise实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。getJSON("/posts.json").then(function(json) {return json.post;}).then(function(post) {// ...});上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。getJSON("/post/1.json").then(function(post) {return getJSON(post.commentURL);}).then(function funcA(comments) {console.log("Resolved: ", comments);}, function funcB(err){console.log("Rejected: ", err);});上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为Resolved,就调用funcA,如果状态变为Rejected,就调用funcB。如果采用箭头函数,上面的代码可以写得更简洁。getJSON("/post/1.json").then(post => getJSON(post.commentURL)).then(comments => console.log("Resolved: ", comments),err => console.log("Rejected: ", err));Promise.prototype.catch()Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。getJSON("/posts.json").then(function(posts) {// ...}).catch(function(error) {// 处理 getJSON 和 前一个回调函数运行时发生的错误console.log('发生错误!', error);});上面代码中,getJSON方法返回一个Promise对象,如果该对象状态变为Resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为Rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。p.then((val) => console.log("fulfilled:", val)).catch((err) => console.log("rejected:", err));// 等同于p.then((val) => console.log("fulfilled:", val)).then(null, (err) => console.log("rejected:", err));下面是一个例子。var promise = new Promise(function(resolve, reject) {throw new Error('test');});promise.catch(function(error) {console.log(error);});// Error: test上面代码中,promise抛出一个错误,就被catch方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。// 写法一var promise = new Promise(function(resolve, reject) {

js es6Iterator和for...of循环js代码

Iterator和for...of循环 Iterator(遍历器)的概念 JavaScript原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6又添加了MapIterator和for...of循环Iterator(遍历器)的概念JavaScript原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。Iterator的遍历过程是这样的。(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。下面是一个模拟next方法返回值的例子。var it = makeIterator(['a', 'b']);it.next() // { value: "a", done: false }it.next() // { value: "b", done: false }it.next() // { value: undefined, done: true }function makeIterator(array) {var nextIndex = 0;return {next: function() {return nextIndex < array.length ?{value: array[nextIndex++], done: false} :{value: undefined, done: true};}};}上面代码定义了一个makeIterator函数,它是一个遍历器生成函数,作用就是返回一个遍历器对象。对数组['a', 'b']执行这个函数,就会返回该数组的遍历器对象(即指针对象)it。指针对象的next方法,用来移动指针。开始时,指针指向数组的开始位置。然后,每次调用next方法,指针就会指向数组的下一个成员。第一次调用,指向a;第二次调用,指向b。next方法返回一个对象,表示当前数据成员的信息。这个对象具有value和done两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next方法。总之,调用指针对象的next方法,就可以遍历事先给定的数据结构。对于遍历器对象来说,done: false和value: undefined属性都是可以省略的,因此上面的makeIterator函数可以简写成下面的形式。function makeIterator(array) {var nextIndex = 0;return {next: function() {return nextIndex < array.length ?{value: array[nextIndex++]} :{done: true};}};}由于Iterator只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构。下面是一个无限运行的遍历器对象的例子。var it = idMaker();it.next().value // '0'it.next().value // '1'it.next().value // '2'// ...function idMaker() {var index = 0;return {next: function() {return {value: index++, done: false};}};}上面的例子中,遍历器生成函数idMaker,返回一个遍历器对象(即指针对象)。但是并没有对应的数据结构,或者说,遍历器对象自己描述了一个数据结构出来。在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被for...of循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。如果使用TypeScript的写法,遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。interface Iterable {[Symbol.iterator]() : Iterator,}interface Iterator {next(value?: any) : IterationResult,}interface IterationResult {value: any,done: boolean,}数据结构的默认Iterator接口Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。一种数据结构只要部署了Iterator接口,我们就称这种数据结构是”可遍历的“(iterable)。ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内。(参见Symbol一章)。const obj = {[Symbol.iterator] : function () {return {next: function () {return {value: 1,done: true};}};}};上面代码中,对象obj是可遍历的(iterable),因为具有Symbol.iterator属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性。在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。let arr = ['a', 'b', 'c'];let iter = arr[Symbol.iterator]();iter.next() // { value: 'a', done: false }iter.next() // { value: 'b', done: false }iter.next() // { value: 'c', done: false }iter.next() // { value: undefined, done: true }上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。上面提到,原生就部署Iterator接口的数据结构有三类,对于这三类数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作Map结构使用,ES5没有Map结构,而ES6原生提供了。一个对象如果要有可被for...of循环调用的Iterator接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。class RangeIterator {constructor(start, stop) {this.value = start;this.stop = stop;}[Symbol.iterator]() { return this; }next() {var value = this.value;if (value < this.stop) {this.value++;return {done: false, value: value};} else {return {done: true, value: undefined};}}}function range(start, stop) {return new RangeIterator(start, stop);}for (var value of range(0, 3)) {console.log(value);}上面代码是一个类部署Iterator接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。下面是通过遍历器实现指针结构的例子。function Obj(value) {this.value = value;this.next = null;}Obj.prototype[Symbol.iterator] = function() {var iterator = {next: next};var current = this;function next() {if (current) {var value = current.value;current = current.next;return {done: false,value: value};} else {return {done: true};}}return iterator;}var one = new Obj(1);var two = new Obj(2);var three = new Obj(3);one.next = two;two.next = three;for (var i of one){console.log(i);}// 1// 2// 3上面代码首先在构造函数的原型链上部署Symbol.iterator方法,调用该方法会返回遍历器对象iterator,调用该对象的next方法,在返回一个值的同时,自动将内部指针移到下一个实例。下面是另一个为对象添加Iterator接口的例子。let obj = {data: [ 'hello', 'world' ],[Symbol.iterator]() {const self = this;let index = 0;return {next() {if (index < self.data.length) {return {value: self.data[index++],done: false};} else {return { value: undefined, done: true };}}};}};对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的Iterator接口。NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];// 或者NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];[...document.querySelectorAll('div')] // 可以执行了下面是类似数组的对象调用数组的Symbol.iterator方法的例子。let iterable = {0: 'a',1: 'b',2: 'c',length: 3,[Symbol.iterator]: Array.prototype[Symbol.iterator]};for (let item of iterable) {console.log(item); // 'a', 'b', 'c'}注意,普通对象部署数组的Symbol.iterator方法,并无效果。let iterable = {a: 'a',b: 'b',c: 'c',length: 3,[Symbol.iterator]: Array.prototype[Symbol.iterator]};for (let item of iterable) {console.log(item); // undefined, undefined, undefined}如果Symbol.iterator方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。var obj = {};obj[Symbol.iterator] = () => 1;[...obj] // TypeError: [] is not a function上面代码中,变量obj的Symbol.iterator方法对应的不是遍历器生成函数,因此报错。有了遍历器接口,数据结构就可以用for...of循环遍历(详见下文),也可以使用while循环遍历。var $iterator = ITERABLE[Symbol.iterator]();var $result = $iterator.next();while (!$result.done) {var x = $result.value;// ...$result = $iterator.next();}上面代码中,ITERABLE代表某种S
标签:

js代码 es6正则的扩展,js正则的扩展

ES6 正则的扩展 RegExp构造函数 在ES5中,RegExp构造函数的参数有两种情况。 第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。 var regexES6 正则的扩展RegExp构造函数在ES5中,RegExp构造函数的参数有两种情况。第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。var regex = new RegExp('xyz', 'i');// 等价于var regex = /xyz/i;第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。var regex = new RegExp(/xyz/i);// 等价于var regex = /xyz/i;但是,ES5不允许此时使用第二个参数,添加修饰符,否则会报错。var regex = new RegExp(/xyz/, 'i');// Uncaught TypeError: Cannot supply flags when constructing one RegExp from anotherES6改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。new RegExp(/abc/ig, 'i').flags// "i"上面代码中,原有正则对象的修饰符是ig,它会被第二个参数i覆盖。字符串的正则方法字符串对象共有4个方法,可以使用正则表达式:match()、replace()、search()和split()。ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。String.prototype.match 调用 RegExp.prototype[Symbol.match]String.prototype.replace 调用 RegExp.prototype[Symbol.replace]String.prototype.search 调用 RegExp.prototype[Symbol.search]String.prototype.split 调用 RegExp.prototype[Symbol.split]u修饰符ES6对正则表达式添加了u修饰符,含义为“Unicode模式”,用来正确处理大于\uFFFF的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。/^\uD83D/u.test('\uD83D\uDC2A')// false/^\uD83D/.test('\uD83D\uDC2A')// true上面代码中,\uD83D\uDC2A是一个四个字节的UTF-16编码,代表一个字符。但是,ES5不支持四个字节的UTF-16编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6就会识别其为一个字符,所以第一行代码结果为false。一旦加上u修饰符号,就会修改下面这些正则表达式的行为。(1)点字符点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF的Unicode字符,点字符不能识别,必须加上u修饰符。var s = '𠮷';/^.$/.test(s) // false/^.$/u.test(s) // true上面代码表示,如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。(2)Unicode字符表示法ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上u修饰符,才能识别。/\u{61}/.test('a') // false/\u{61}/u.test('a') // true/\u{20BB7}/u.test('𠮷') // true上面代码表示,如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配61个连续的u。(3)量词使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的Unicode字符。/a{2}/.test('aa') // true/a{2}/u.test('aa') // true/𠮷{2}/.test('𠮷𠮷') // false/𠮷{2}/u.test('𠮷𠮷') // true另外,只有在使用u修饰符的情况下,Unicode表达式当中的大括号才会被正确解读,否则会被解读为量词。/^\u{3}$/.test('uuu') // true上面代码中,由于正则表达式没有u修饰符,所以大括号被解读为量词。加上u修饰符,就会被解读为Unicode表达式。(4)预定义模式u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的Unicode字符。/^\S$/.test('𠮷') // false/^\S$/u.test('𠮷') // true上面代码的\S是预定义模式,匹配所有不是空格的字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的Unicode字符。利用这一点,可以写出一个正确返回字符串长度的函数。function codePointLength(text) {var result = text.match(/[\s\S]/gu);return result ? result.length : 0;}var s = '𠮷𠮷';s.length // 4codePointLength(s) // 2(5)i修饰符有些Unicode字符的编码不同,但是字型很相近,比如,\u004B与\u212A都是大写的K。/[a-z]/i.test('\u212A') // false/[a-z]/iu.test('\u212A') // true上面代码中,不加u修饰符,就无法识别非规范的K字符。y 修饰符除了u修饰符,ES6还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。var s = 'aaa_aa_a';var r1 = /a+/g;var r2 = /a+/y;r1.exec(s) // ["aaa"]r2.exec(s) // ["aaa"]r1.exec(s) // ["aa"]r2.exec(s) // null上面代码有两个正则表达式,一个使用g修饰符,另一个使用y修饰符。这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是_aa_a。由于g修饰没有位置要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null。如果改一下正则表达式,保证每次都能头部匹配,y修饰符就会返回结果了。var s = 'aaa_aa_a';var r = /a+_/y;r.exec(s) // ["aaa_"]r.exec(s) // ["aa_"]上面代码每次匹配,都是从剩余字符串的头部开始。使用lastIndex属性,可以更好地说明y修饰符。const REGEX = /a/g;// 指定从2号位置(y)开始匹配REGEX.lastIndex = 2;// 匹配成功const match = REGEX.exec('xaya');// 在3号位置匹配成功match.index // 3// 下一次匹配从4号位开始REGEX.lastIndex // 4// 4号位开始匹配失败REGEX.exec('xaxa') // null上面代码中,lastIndex属性指定每次搜索的开始位置,g修饰符从这个位置开始向后搜索,直到发现匹配为止。y修饰符同样遵守lastIndex属性,但是要求必须在lastIndex指定的位置发现匹配。const REGEX = /a/y;// 指定从2号位置开始匹配REGEX.lastIndex = 2;// 不是粘连,匹配失败REGEX.exec('xaya') // null// 指定从3号位置开始匹配REGEX.lastIndex = 3;// 3号位置是粘连,匹配成功const match = REGEX.exec('xaxa');match.index // 3REGEX.lastIndex // 4进一步说,y修饰符号隐含了头部匹配的标志^。/b/y.exec('aba')// null上面代码由于不能保证头部匹配,所以返回null。y修饰符的设计本意,就是让头部匹配的标志^在全局匹配中都有效。在split方法中使用y修饰符,原字符串必须以分隔符开头。这也意味着,只要匹配成功,数组的第一个成员肯定是空字符串。// 没有找到匹配'x##'.split(/#/y)// [ 'x##' ]// 找到两个匹配'##x'.split(/#/y)// [ '', '', 'x' ]后续的分隔符只有紧跟前面的分隔符,才会被识别。'#x#'.split(/#/y)// [ '', 'x#' ]'##'.split(/#/y)// [ '', '', '' ]下面是字符串对象的replace方法的例子。const REGEX = /a/gy;'aaxa'.replace(REGEX, '-') // '--xa'上面代码中,最后一个a因为不是出现下一次匹配的头部,所以不会被替换。单单一个y修饰符对match方法,只能返回第一个匹配,必须与g修饰符联用,才能返回所有匹配。'a1a2a3'.match(/a\d/y) // ["a1"]'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]y修饰符的一个应用,是从字符串提取token(词元),y修饰符确保了匹配之间不会有漏掉的字符。const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;const TOKEN_G= /\s*(\+|[0-9]+)\s*/g;tokenize(TOKEN_Y, '3 + 4')// [ '3', '+', '4' ]tokenize(TOKEN_G, '3 + 4')// [ '3', '+', '4' ]function tokenize(TOKEN_REGEX, str) {let result = [];let match;while (match = TOKEN_REGEX.exec(str)) {result.push(match[1]);}return result;}上面代码中,如果字符串里面没有非法字符,y修饰符与g修饰符的提取结果是一样的。但是,一旦出现非法字符,两者的行为就不一样了。tokenize(TOKEN_Y, '3x + 4')// [ '3' ]tokenize(TOKEN_G, '3x + 4')// [ '3', '+', '4' ]上面代码中,g修饰符会忽略非法字符,而y修饰符不会,这样就很容易发现错误。sticky属性与y修饰符相匹配,ES6的正则对象多了sticky属性,表示是否设置了y修饰符。var r = /hello\d/y;r.sticky // trueflags属性ES6为正则表达式新增了flags属性,会返回正则表达式的修饰符。// ES5的source属性// 返回正则表达式的正文/abc/ig.source// "abc"// ES6的flags属性// 返回正则表达式的修饰符/abc/ig.flags// 'gi'RegExp.escape()字符串必须转义,才能作为正则模式。function escapeRegExp(str) {return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');}let str = '/path/to/resource.html?search=query';escapeRegExp(str)// "\/path\/to\/resource\.html\?search=query"上面代码中,str是一个正常字符串,必须使用反斜杠对其中的特殊字符转义,才能用来作为一个正则匹配的模式。已经有提议将这个需求标准化,作为RegExp对象的静态方法RegExp.escape(),放入ES7。2021年7月31日,TC39认为,这个方法有安全风险,又不愿这个方法变得过于复杂,没有同意将其列入ES7,但这不失为一个真实的需求。RegExp.escape('The Quick Brown Fox');// "The Quick Brown Fox"RegExp.escape('Buy it. use it. break it. fix it.');// "Buy it\. use it\. break it\. fix it\."RegExp.escape('(*.*)');// "\(\*\.\*\)"字符串转义以后,可以使用RegExp构造函数生成正则模式。var str = 'hello. how are you?';var regex = new RegExp(RegExp.escape(str), 'g');assert.equal(String(regex), '/hello\. how are you\?/g');目前,该方法可以用上文的escapeRegExp函数或者垫片模块regexp.escape实现。var escape = require('regexp.escape');

js代码 es6数组的扩展,js数组的扩展

ES6 数组的扩展 Array.from() Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6 数组的扩展Array.from()Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。下面是一个类似数组的对象,Array.from将它转为真正的数组。let arrayLike = {'0': 'a','1': 'b','2': 'c',length: 3};// ES5的写法var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']// ES6的写法let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']实际应用中,常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。// NodeList对象let ps = document.querySelectorAll('p');Array.from(ps).forEach(function (p) {console.log(p);});// arguments对象function foo() {var args = Array.from(arguments);// ...}上面代码中,querySelectorAll方法返回的是一个类似数组的对象,只有将这个对象转为真正的数组,才能使用forEach方法。只要是部署了Iterator接口的数据结构,Array.from都能将其转为数组。Array.from('hello')// ['h', 'e', 'l', 'l', 'o']let namesSet = new Set(['a', 'b'])Array.from(namesSet) // ['a', 'b']上面代码中,字符串和Set结构都具有Iterator接口,因此可以被Array.from转为真正的数组。如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。Array.from([1, 2, 3])// [1, 2, 3]值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。// arguments对象function foo() {var args = [...arguments];}// NodeList对象[...document.querySelectorAll('div')]扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法则是还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。Array.from({ length: 3 });// [ undefined, undefined, undefined ]上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。const toArray = (() =>Array.from ? Array.from : obj => [].slice.call(obj))();Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。Array.from(arrayLike, x => x * x);// 等同于Array.from(arrayLike).map(x => x * x);Array.from([1, 2, 3], (x) => x * x)// [1, 4, 9]下面的例子是取出一组DOM节点的文本内容。let spans = document.querySelectorAll('span.name');// map()let names1 = Array.prototype.map.call(spans, s => s.textContent);// Array.from()let names2 = Array.from(spans, s => s.textContent)下面的例子将数组中布尔值为false的成员转为0。Array.from([1, , 2, , 3], (n) => n || 0)// [1, 0, 2, 0, 3]另一个例子是返回各种数据的类型。function typesOf () {return Array.from(arguments, value => typeof value)}typesOf(null, [], NaN)// ['object', 'object', 'number']如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this。Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。Array.from({ length: 2 }, () => 'jack')// ['jack', 'jack']上面代码中,Array.from的第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活。Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种Unicode字符,可以避免JavaScript将大于\uFFFF的Unicode字符,算作两个字符的bug。function countSymbols(string) {return Array.from(string).length;}Array.of()Array.of方法用于将一组值,转换为数组。Array.of(3, 11, 8) // [3,11,8]Array.of(3) // [3]Array.of(3).length // 1这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。Array() // []Array(3) // [, , ,]Array(3, 11, 8) // [3, 11, 8]上面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于2个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。Array.of() // []Array.of(undefined) // [undefined]Array.of(1) // [1]Array.of(1, 2) // [1, 2]Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。Array.of方法可以用下面的代码模拟实现。function ArrayOf(){return [].slice.call(arguments);}数组实例的copyWithin()数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。Array.prototype.copyWithin(target, start = 0, end = this.length)它接受三个参数。target(必需):从该位置开始替换数据。start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。这三个参数都应该是数值,如果不是,会自动转为数值。[1, 2, 3, 4, 5].copyWithin(0, 3)// [4, 5, 3, 4, 5]上面代码表示将从3号位直到数组结束的成员(4和5),复制到从0号位开始的位置,结果覆盖了原来的1和2。下面是更多例子。// 将3号位复制到0号位[1, 2, 3, 4, 5].copyWithin(0, 3, 4)// [4, 2, 3, 4, 5]// -2相当于3号位,-1相当于4号位[1, 2, 3, 4, 5].copyWithin(0, -2, -1)// [4, 2, 3, 4, 5]// 将3号位复制到0号位[].copyWithin.call({length: 5, 3: 1}, 0, 3)// {0: 1, 3: 1, length: 5}// 将2号位到数组结束,复制到0号位var i32a = new Int32Array([1, 2, 3, 4, 5]);i32a.copyWithin(0, 2);// Int32Array [3, 4, 5, 4, 5]// 对于没有部署TypedArray的copyWithin方法的平台// 需要采用下面的写法[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);// Int32Array [4, 2, 3, 4, 5]数组实例的find()和findIndex()数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。[1, 4, -5, 10].find((n) => n < 0)// -5上面代码找出数组中第一个小于0的成员。[1, 5, 10, 15].find(function(value, index, arr) {return value > 9;}) // 10上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。[1, 5, 10, 15].findIndex(function(value, index, arr) {return value > 9;}) // 2这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。另外,这两个方法都可以发现NaN,弥补了数组的IndexOf方法的不足。[NaN].indexOf(NaN)// -1[NaN].findIndex(y => Object.is(NaN, y))// 0上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。数组实例的fill()fill方法使用给定值,填充一个数组。['a', 'b', 'c'].fill(7)// [7, 7, 7]new Array(3).fill(7)// [7, 7, 7]上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。['a', 'b', 'c'].fill(7, 1, 2)// ['a', 7, 'c']上面代码表示,fill方法从1号位开始,向原数组填充7,到2号位之前结束。数组实例的entries(),keys()和values()ES6提供三个新的方法‡‡entries(),keys()和values()‡‡用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。for (let index of ['a', 'b'].keys()) {console.log(index);}// 0// 1for (let elem of ['a', 'b'].values()) {console.log(elem);}// 'a'// 'b'for (let [index, elem] of ['a', 'b'].entries()) {console.log(index, elem);}// 0 "a"// 1 "b"如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。let letter = ['a', 'b', 'c'];let entries = letter.entries();console.log(entries.next().value); //
标签:

js代码 es6编程风格,js编程风格

ES6 编程风格 本章探讨如何将ES6的新语法,运用到编码实践之中,与传统的JavaScript语法结合在一起,写出合理的、易于阅读和维护的代码。 多家公司和组织已经公开了它们的风格规范,具体ES6 编程风格本章探讨如何将ES6的新语法,运用到编码实践之中,与传统的JavaScript语法结合在一起,写出合理的、易于阅读和维护的代码。多家公司和组织已经公开了它们的风格规范,具体可参阅jscs.info,下面的内容主要参考了Airbnb的JavaScript风格规范。块级作用域(1)let 取代 varES6提出了两个新的声明变量的命令:let和const。其中,let完全可以取代var,因为两者语义相同,而且let没有副作用。'use strict';if (true) {let x = 'hello';}for (let i = 0; i < 10; i++) {console.log(i);}上面代码如果用var替代let,实际上就声明了两个全局变量,这显然不是本意。变量应该只在其声明的代码块内有效,var命令做不到这一点。var命令存在变量提升效用,let命令没有这个问题。'use strict';if(true) {console.log(x); // ReferenceErrorlet x = 'hello';}上面代码如果使用var替代let,console.log那一行就不会报错,而是会输出undefined,因为变量声明提升到代码块的头部。这违反了变量先声明后使用的原则。所以,建议不再使用var命令,而是使用let命令取代。(2)全局常量和线程安全在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。const优于let有几个原因。一个是const可以提醒阅读程序的人,这个变量不应该改变;另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提供程序的运行效率,也就是说let和const的本质区别,其实是编译器内部的处理不同。// badvar a = 1, b = 2, c = 3;// goodconst a = 1;const b = 2;const c = 3;// bestconst [a, b, c] = [1, 2, 3];const声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。所有的函数都应该设置为常量。长远来看,JavaScript可能会有多线程的实现(比如Intel的River Trail那一类的项目),这时let表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。字符串静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。// badconst a = "foobar";const b = 'foo' + a + 'bar';// acceptableconst c = `foobar`;// goodconst a = 'foobar';const b = `foo${a}bar`;const c = 'foobar';解构赋值使用数组成员对变量赋值时,优先使用解构赋值。const arr = [1, 2, 3, 4];// badconst first = arr[0];const second = arr[1];// goodconst [first, second] = arr;函数的参数如果是对象的成员,优先使用解构赋值。// badfunction getFullName(user) {const firstName = user.firstName;const lastName = user.lastName;}// goodfunction getFullName(obj) {const { firstName, lastName } = obj;}// bestfunction getFullName({ firstName, lastName }) {}如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。// badfunction processInput(input) {return [left, right, top, bottom];}// goodfunction processInput(input) {return { left, right, top, bottom };}const { left, right } = processInput(input);对象单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。// badconst a = { k1: v1, k2: v2, };const b = {k1: v1,k2: v2};// goodconst a = { k1: v1, k2: v2 };const b = {k1: v1,k2: v2,};对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。// badconst a = {};a.x = 3;// if reshape unavoidableconst a = {};Object.assign(a, { x: 3 });// goodconst a = { x: null };a.x = 3;如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。// badconst obj = {id: 5,name: 'San Francisco',};obj[getKey('enabled')] = true;// goodconst obj = {id: 5,name: 'San Francisco',[getKey('enabled')]: true,};上面代码中,对象obj的最后一个属性名,需要计算得到。这时最好采用属性表达式,在新建obj的时候,将该属性与其他属性定义在一起。这样一来,所有属性就在一个地方定义了。另外,对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。var ref = 'some value';// badconst atom = {ref: ref,value: 1,addValue: function (value) {return atom.value + value;},};// goodconst atom = {ref,value: 1,addValue(value) {return atom.value + value;},};数组使用扩展运算符(...)拷贝数组。// badconst len = items.length;const itemsCopy = [];let i;for (i = 0; i < len; i++) {itemsCopy[i] = items[i];}// goodconst itemsCopy = [...items];使用Array.from方法,将类似数组的对象转为数组。const foo = document.querySelectorAll('.foo');const nodes = Array.from(foo);函数立即执行函数可以写成箭头函数的形式。(() => {console.log('Welcome to the Internet.');})();那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了this。// bad[1, 2, 3].map(function (x) {return x * x;});// good[1, 2, 3].map((x) => {return x * x;});// best[1, 2, 3].map(x => x * x);箭头函数取代Function.prototype.bind,不应再用self/_this/that绑定 this。// badconst self = this;const boundMethod = function(...params) {return method.apply(self, params);}// acceptableconst boundMethod = method.bind(this);// bestconst boundMethod = (...params) => method.apply(this, params);简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。// badfunction divide(a, b, option = false ) {}// goodfunction divide(a, b, { option = false } = {}) {}不要在函数体内使用arguments变量,使用rest运算符(...)代替。因为rest运算符显式表明你想要获取参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组。// badfunction concatenateAll() {const args = Array.prototype.slice.call(arguments);return args.join('');}// goodfunction concatenateAll(...args) {return args.join('');}使用默认值语法设置函数参数的默认值。// badfunction handleThings(opts) {opts = opts || {};}// goodfunction handleThings(opts = {}) {// ...}Map结构注意区分Object和Map,只有模拟现实世界的实体对象时,才使用Object。如果只是需要key: value的数据结构,使用Map结构。因为Map有内建的遍历机制。let map = new Map(arr);for (let key of map.keys()) {console.log(key);}for (let value of map.values()) {console.log(value);}for (let item of map.entries()) {console.log(item[0], item[1]);}Class总是用Class,取代需要prototype的操作。因为Class的写法更简洁,更易于理解。// badfunction Queue(contents = []) {this._queue = [...contents];}Queue.prototype.pop = function() {const value = this._queue[0];this._queue.splice(0, 1);return value;}// goodclass Queue {constructor(contents = []) {this._queue = [...contents];}pop() {const value = this._queue[0];this._queue.splice(0, 1);return value;}}使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。// badconst inherits = require('inherits');function PeekableQueue(contents) {Queue.apply(this, contents);}inherits(PeekableQueue, Queue);PeekableQueue.prototype.peek = function() {return this._queue[0];}// goodclass PeekableQueue extends Queue {peek() {return this._queue[0];}}模块首先,Module语法是JavaScript模块的标准写法,坚持使用这种写法。使用import取代require。// badconst moduleA = require('moduleA');const func1 = moduleA.func1;const func2 = moduleA.func2;// goodimport { func1, func2 } from 'moduleA';使用export取代module.exports。// commonJS的写法var React = require('react');var Breadcrumbs = React.createClass({render() {return <nav />;}});module.exports = Breadcrumbs;// ES6的写法import React from 'react';const Breadcrumbs = React.createClass({render() {return <nav />;}});export default Breadcrumbs如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,不要export default与普通的export同时使用。不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。// badimport * as myObject './importModule';// goodimport myObject from './importModule';如果模块默认输出一个函数,函数名的首字母应该小写。function makeStyleGuide() {}export default makeStyleGuide;如果模块默认输出一个对象,对象名的首字母应该大写。const StyleGuide = {es6: {}};export default StyleGuide;ESLint的使用ESLint是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。首
标签:

js代码 es6数值的扩展,js数值的扩展

ES6 数值的扩展 二进制和八进制表示法 ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。 0b111110111 === 503 // true 0oES6 数值的扩展二进制和八进制表示法ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。0b111110111 === 503 // true0o767 === 503 // true从ES5开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6进一步明确,要使用前缀0o表示。// 非严格模式(function(){console.log(0o11 === 011);})() // true// 严格模式(function(){'use strict';console.log(0o11 === 011);})() // Uncaught SyntaxError: Octal literals are not allowed in strict mode.如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法。Number('0b111')// 7Number('0o10')// 8Number.isFinite(), Number.isNaN()ES6在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。Number.isFinite()用来检查一个数值是否为有限的(finite)。Number.isFinite(15); // trueNumber.isFinite(0.8); // trueNumber.isFinite(NaN); // falseNumber.isFinite(Infinity); // falseNumber.isFinite(-Infinity); // falseNumber.isFinite('foo'); // falseNumber.isFinite('15'); // falseNumber.isFinite(true); // falseES5可以通过下面的代码,部署Number.isFinite方法。(function (global) {var global_isFinite = global.isFinite;Object.defineProperty(Number, 'isFinite', {value: function isFinite(value) {return typeof value === 'number' && global_isFinite(value);},configurable: true,enumerable: false,writable: true});})(this);Number.isNaN()用来检查一个值是否为NaN。Number.isNaN(NaN) // trueNumber.isNaN(15) // falseNumber.isNaN('15') // falseNumber.isNaN(true) // falseNumber.isNaN(9/NaN) // trueNumber.isNaN('true'/0) // trueNumber.isNaN('true'/'true') // trueES5通过下面的代码,部署Number.isNaN()。(function (global) {var global_isNaN = global.isNaN;Object.defineProperty(Number, 'isNaN', {value: function isNaN(value) {return typeof value === 'number' && global_isNaN(value);},configurable: true,enumerable: false,writable: true});})(this);它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回false。isFinite(25) // trueisFinite("25") // trueNumber.isFinite(25) // trueNumber.isFinite("25") // falseisNaN(NaN) // trueisNaN("NaN") // trueNumber.isNaN(NaN) // trueNumber.isNaN("NaN") // falseNumber.parseInt(), Number.parseFloat()ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。// ES5的写法parseInt('12.34') // 12parseFloat('123.45#') // 123.45// ES6的写法Number.parseInt('12.34') // 12Number.parseFloat('123.45#') // 123.45这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。Number.parseInt === parseInt // trueNumber.parseFloat === parseFloat // trueNumber.isInteger()Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值。Number.isInteger(25) // trueNumber.isInteger(25.0) // trueNumber.isInteger(25.1) // falseNumber.isInteger("15") // falseNumber.isInteger(true) // falseES5可以通过下面的代码,部署Number.isInteger()。(function (global) {var floor = Math.floor,isFinite = global.isFinite;Object.defineProperty(Number, 'isInteger', {value: function isInteger(value) {return typeof value === 'number' && isFinite(value) &&value > -9007199254740992 && value < 9007199254740992 &&floor(value) === value;},configurable: true,enumerable: false,writable: true});})(this);Number.EPSILONES6在Number对象上面,新增一个极小的常量Number.EPSILON。Number.EPSILON// 2.220446049250313e-16Number.EPSILON.toFixed(20)// '0.00000000000000022204'引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。0.1 + 0.2// 0.300000000000000040.1 + 0.2 - 0.3// 5.551115123125783e-175.551115123125783e-17.toFixed(20)// '0.00000000000000005551'但是如果这个误差能够小于Number.EPSILON,我们就可以认为得到了正确结果。5.551115123125783e-17 < Number.EPSILON// true因此,Number.EPSILON的实质是一个可以接受的误差范围。function withinErrorMargin (left, right) {return Math.abs(left - right) < Number.EPSILON;}withinErrorMargin(0.1 + 0.2, 0.3)// truewithinErrorMargin(0.2 + 0.2, 0.3)// false上面的代码为浮点数运算,部署了一个误差检查函数。安全整数和Number.isSafeInteger()JavaScript能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。Math.pow(2, 53) // 90071992547409929007199254740992// 90071992547409929007199254740993// 9007199254740992Math.pow(2, 53) === Math.pow(2, 53) + 1// true上面代码中,超出2的53次方之后,一个数就不精确了。ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1// trueNumber.MAX_SAFE_INTEGER === 9007199254740991// trueNumber.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER// trueNumber.MIN_SAFE_INTEGER === -9007199254740991// true上面代码中,可以看到JavaScript能够精确表示的极限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。Number.isSafeInteger('a') // falseNumber.isSafeInteger(null) // falseNumber.isSafeInteger(NaN) // falseNumber.isSafeInteger(Infinity) // falseNumber.isSafeInteger(-Infinity) // falseNumber.isSafeInteger(3) // trueNumber.isSafeInteger(1.2) // falseNumber.isSafeInteger(9007199254740990) // trueNumber.isSafeInteger(9007199254740992) // falseNumber.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // falseNumber.isSafeInteger(Number.MIN_SAFE_INTEGER) // trueNumber.isSafeInteger(Number.MAX_SAFE_INTEGER) // trueNumber.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false这个函数的实现很简单,就是跟安全整数的两个边界值比较一下。Number.isSafeInteger = function (n) {return (typeof n === 'number' &&Math.round(n) === n &&Number.MIN_SAFE_INTEGER <= n &&n <= Number.MAX_SAFE_INTEGER);}实际使用这个函数时,需要注意。验证运算结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值。Number.isSafeInteger(9007199254740993)// falseNumber.isSafeInteger(990)// trueNumber.isSafeInteger(9007199254740993 - 990)// true9007199254740993 - 990// 返回结果 9007199254740002// 正确答案应该是 9007199254740003上面代码中,9007199254740993不是一个安全整数,但是Number.isSafeInteger会返回结果,显示计算结果是安全的。这是因为,这个数超出了精度范围,导致在计算机内部,以9007199254740992的形式储存。9007199254740993 === 9007199254740992// true所以,如果只验证运算结果是否为安全整数,很可能得到错误结果。下面的函数可以同时验证两个运算数和运算结果。function trusty (left, right, result) {if (Number.isSafeInteger(left) &&Number.isSafeInteger(right) &&Number.isSafeInteger(result)) {return result;}throw new RangeError('Operation cannot be trusted!');}trusty(9007199254740993, 990, 9007199254740993 - 990)// RangeError: Operation cannot be trusted!trusty(1, 2, 3)// 3Math对象的扩展ES6在Math对象上新增了17个与数学相关的方法。所有这些方法都是静态方法,只能在Math对象上调用。Math.trunc()Math.trunc方法用于去除一个数的小数部分,返回整数部分。Math.trunc(4.1) // 4Math.trunc(4.9) // 4Math.trunc(-4.1) // -4Math.trunc(-4.9) // -4Math.trunc(-0.1234) // -0对于非数值,Math.trunc内部使用Number方法将其先转为数值。Math.trunc('123.456')// 123对于空值和无法截取整数的值,返回NaN。Math.trunc(NaN);// NaNMath.trunc('foo');// NaNMath.trunc();// NaN对于没有部署这个方法的环境,可以用下面的代码模拟。Math.trunc = Math.trunc || function(x) {return x < 0 ? Math.ceil(x) : Math.floor(x);};Math.sign()Math.sign方法用来判断一个数到底是正数、负数、还是零。它会返回五种值。参数为正数,返回+1;参数为负数,返回-1;参数为0,返回0;参数为-0,返回-0;其他值,返回NaN。Math.sign(-5) // -1Math.sign(5) // +1Math.sign(0) // +0Math.sign(-0) // -0Math.sign(NaN) // NaNMath.sign('foo'); // NaNMath.sign();// NaN对于没有部署这个方法的环境,可以用下面的代码模拟。Math.sign = Math.sign || function(x) {x = +x; // convert to a numberif (x === 0 || isNaN(x)) {return x;}return x > 0 ? 1 : -1;};

js es6Decoratorjs代码

ES6 修饰器 类的修饰 修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个提案,目前Babel转码器已经支持。 修饰器对类的行为的改变,是代码编译时发生的,而不是在运ES6 修饰器类的修饰修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个提案,目前Babel转码器已经支持。修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。function testable(target) {target.isTestable = true;}@testableclass MyTestableClass {}console.log(MyTestableClass.isTestable) // true上面代码中,@testable就是一个修饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable。基本上,修饰器的行为就是下面这样。@decoratorclass A {}// 等同于class A {}A = decorator(A) || A;也就是说,修饰器本质就是编译时执行的函数。修饰器函数的第一个参数,就是所要修饰的目标类。function testable(target) {// ...}上面代码中,testable函数的参数target,就是会被修饰的类。如果觉得一个参数不够用,可以在修饰器外面再封装一层函数。function testable(isTestable) {return function(target) {target.isTestable = isTestable;}}@testable(true)class MyTestableClass {}MyTestableClass.isTestable // true@testable(false)class MyClass {}MyClass.isTestable // false上面代码中,修饰器testable可以接受参数,这就等于可以修改修饰器的行为。前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的prototype对象操作。function testable(target) {target.prototype.isTestable = true;}@testableclass MyTestableClass {}let obj = new MyTestableClass();obj.isTestable // true上面代码中,修饰器函数testable是在目标类的prototype对象上添加属性,因此就可以在实例上调用。下面是另外一个例子。// mixins.jsexport function mixins(...list) {return function (target) {Object.assign(target.prototype, ...list)}}// main.jsimport { mixins } from './mixins'const Foo = {foo() { console.log('foo') }};@mixins(Foo)class MyClass {}let obj = new MyClass();obj.foo() // 'foo'上面代码通过修饰器mixins,把Foo类的方法添加到了MyClass的实例上面。可以用Object.assign()模拟这个功能。const Foo = {foo() { console.log('foo') }};class MyClass {}Object.assign(MyClass.prototype, Foo);let obj = new MyClass();obj.foo() // 'foo'方法的修饰修饰器不仅可以修饰类,还可以修饰类的属性。class Person {@readonlyname() { return `${this.first} ${this.last}` }}上面代码中,修饰器readonly用来修饰“类”的name方法。此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。function readonly(target, name, descriptor){// descriptor对象原来的值如下// {//value: specifiedFunction,//enumerable: false,//configurable: true,//writable: true// };descriptor.writable = false;return descriptor;}readonly(Person.prototype, 'name', descriptor);// 类似于Object.defineProperty(Person.prototype, 'name', descriptor);上面代码说明,修饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。下面是另一个例子,修改属性描述对象的enumerable属性,使得该属性不可遍历。class Person {@nonenumerableget kidCount() { return this.children.length; }}function nonenumerable(target, name, descriptor) {descriptor.enumerable = false;return descriptor;}下面的@log修饰器,可以起到输出日志的作用。class Math {@logadd(a, b) {return a + b;}}function log(target, name, descriptor) {var oldValue = descriptor.value;descriptor.value = function() {console.log(`Calling "${name}" with`, arguments);return oldValue.apply(null, arguments);};return descriptor;}const math = new Math();// passed parameters should get logged nowmath.add(2, 4);上面代码中,@log修饰器的作用就是在执行原始的操作之前,执行一次console.log,从而达到输出日志的目的。修饰器有注释的作用。@testableclass Person {@readonly@nonenumerablename() { return `${this.first} ${this.last}` }}从上面代码中,我们一眼就能看出,Person类是可测试的,而name方法是只读和不可枚举的。如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。function dec(id){console.log('evaluated', id);return (target, property, descriptor) => console.log('executed', id);}class Example {@dec(1)@dec(2)method(){}}// evaluated 1// evaluated 2// executed 2// executed 1上面代码中,外层修饰器@dec(1)先进入,但是内层修饰器@dec(2)先执行。除了注释,修饰器还能用来类型检查。所以,对于类来说,这项功能相当有用。从长期来看,它将是JavaScript代码静态分析的重要工具。为什么修饰器不能用于函数?修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。var counter = 0;var add = function () {counter++;};@addfunction foo() {}上面的代码,意图是执行后counter等于1,但是实际上结果是counter等于0。因为函数提升,使得实际执行的代码是下面这样。var counter;var add;@addfunction foo() {}counter = 0;add = function () {counter++;};下面是另一个例子。var readOnly = require("some-decorator");@readOnlyfunction foo() {}上面代码也有问题,因为实际执行是下面这样。var readOnly;@readOnlyfunction foo() {}readOnly = require("some-decorator");总之,由于存在函数提升,使得修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。core-decorators.jscore-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。(1)@autobindautobind修饰器使得方法中的this对象,绑定原始对象。import { autobind } from 'core-decorators';class Person {@autobindgetPerson() {return this;}}let person = new Person();let getPerson = person.getPerson;getPerson() === person;// true(2)@readonlyreadonly修饰器使得属性或方法不可写。import { readonly } from 'core-decorators';class Meal {@readonlyentree = 'steak';}var dinner = new Meal();dinner.entree = 'salmon';// Cannot assign to read only property 'entree' of [object Object](3)@overrideoverride修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。import { override } from 'core-decorators';class Parent {speak(first, second) {}}class Child extends Parent {@overridespeak() {}// SyntaxError: Child#speak() does not properly override Parent#speak(first, second)}// orclass Child extends Parent {@overridespeaks() {}// SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.////Did you mean "speak"?}(4)@deprecate (别名@deprecated)deprecate或deprecated修饰器在控制台显示一条警告,表示该方法将废除。import { deprecate } from 'core-decorators';class Person {@deprecatefacepalm() {}@deprecate('We stopped facepalming')facepalmHard() {}@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })facepalmHarder() {}}let person = new Person();person.facepalm();// DEPRECATION Person#facepalm: This function will be removed in future versions.person.facepalmHard();// DEPRECATION Person#facepalmHard: We stopped facepalmingperson.facepalmHarder();// DEPRECATION Person#facepalmHarder: We stopped facepalming////See http://knowyourmeme.com/memes/facepalm for more details.//(5)@suppressWarningssuppressWarnings修饰器抑制decorated修饰器导致的console.warn()调用。但是,异步代码发出的调用除外。import { suppressWarnings } from 'core-decorators';class Person {@deprecatedfacepalm() {}@suppressWarningsfacepalmWithoutWarning() {this.facepalm();}}let person = new Person();person.facepalmWithoutWarning();// no warning is logged使用修饰器实现自动发布事件我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。import postal from "postal/lib/postal.lodash";export default function publish(topic, channel) {return function(target, name, descriptor) {const fn = descriptor.value;descriptor.value = function() {let value = fn.apply(this, arguments);postal.channel(channel || target.channel || "/").publish(topic, value);};};}上面代码定义了一个名为publish的修饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。它使用的事件“发布/订阅”库是Postal.js。它的用法如下。import publish from "path/to/decorators/publish";class FooComponent {@publish("foo.some.message", "component")someMethod() {return {my: "data"};}@publish("foo.some.other")anotherMethod() {// ...}}以后,只要调用someMethod或者anotherMethod,就会自动发出一个事件。let foo = new FooComponent();foo.someMethod() // 在"component"频道发布"foo.some.message"事件,附带的数据是{ my: "data" }foo.anotherMethod() // 在"/"频道发布"foo.some.other"事件,不附带数据Mixin在修饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。请看下面的例子。const Foo = {foo() { console.log('foo') }};class MyClass {}Object.assign(MyClass.prototype, Foo);let obj = new MyClass();obj.foo() // 'foo'上面代码之中,对象Foo有一个foo方法,通过Object.assign方法,可以将foo方法“混入”MyClass类,导致MyClass的实例obj对象都具有foo方法。这就是“混入”模式的一个简单实现。下面,我们部署一个通用脚本

js代码 es6ES6入门文档,jsES6入门文档

ECMAScript 6简介 ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以ECMAScript 6简介ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,已经在2021年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。ECMAScript和JavaScript的关系一个常见的问题是,ECMAScript和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方言还有Jscript和ActionScript)。日常场合,这两个词是可以互换的。ES6与ECMAScript 2021的关系媒体里面经常可以看到”ECMAScript 2021“这个词,它与ES6是什么关系呢?2021年,ECMAScript 5.1版发布后,就开始制定6.0版了。因此,”ES6”这个词的原意,就是指JavaScript语言的下一个版本。但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布6.0版,过一段时间再发6.1版,然后是6.2版、6.3版等等。但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。标准委员会最终决定,标准在每年的6月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的6月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。ES6的第一个版本,就这样在2021年6月发布了,正式名称就是《ECMAScript 2021标准》(简称ES2021)。2021年6月,小幅修订的《ECMAScript 2021标准》(简称ES2021)如期发布,这个版本可以看作是ES6.1版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2021年6月将发布ES2021标准。因此,ES6既是一个历史名词,也是一个泛指,含义是5.1版以后的JavaScript的下一代标准,涵盖了ES2021、ES2021、ES2021等等,而ES2021则是正式名称,特指该年发布的正式版本的语言标准。本书中提到“ES6”的地方,一般是指ES2021标准,但有时也是泛指“下一代JavaScript语言”。语法提案的批准流程任何人都可以向TC39标准委员会提案。一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由TC39委员会批准。Stage 0 - Strawman(展示阶段)Stage 1 - Proposal(征求意见阶段)Stage 2 - Draft(草案阶段)Stage 3 - Candidate(候选人阶段)Stage 4 - Finished(定案阶段)一个提案只要能进入Stage 2,就差不多等于肯定会包括在以后的正式标准里面。ECMAScript当前的所有提案,可以在TC39的官方网站Github.com/tc39/ecma262查看。本书的写作目标之一,是跟踪ECMAScript语言的最新进展,介绍5.1版本以后所有的新语法。对于那些明确将要列入标准的新语法,尤其是那些Babel转码器(详见后文)已经支持的功能,也将予以介绍。ECMAScript的历史ES6从开始制定到最后发布,整整用了15年。前面提到,ECMAScript 1.0是1997年发布的,接下来的两年,连续发布了ECMAScript 2.0(1998年6月)和ECMAScript 3.0(1999年12月)。3.0版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了JavaScript语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习JavaScript,其实就是在学3.0版的语法。2000年,ECMAScript 4.0开始酝酿。这个版本最后没有通过,但是它的大部分内容被ES6继承了。因此,ES6制定的起点其实是2000年。为什么ES4没有通过呢?因为这个版本太激进了,对ES3做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA的第39号技术专家委员会(Technical Committee 39,简称TC39)负责制订ECMAScript标准,成员包括Microsoft、Mozilla、Google等大公司。2021年10月,ECMAScript 4.0版草案发布,本来预计次年8月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以Yahoo、Microsoft、Google为首的大公司,反对JavaScript的大幅升级,主张小幅改动;以JavaScript创造者Brendan Eich为首的Mozilla公司,则坚持当前的草案。2021年7月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA开会决定,中止ECMAScript 4.0的开发,将其中涉及现有功能改善的一小部分,发布为ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为Harmony(和谐)。会后不久,ECMAScript 3.1就改名为ECMAScript 5。2021年12月,ECMAScript 5.0版正式发布。Harmony项目则一分为二,一些较为可行的设想定名为JavaScript.next继续开发,后来演变成ECMAScript 6;一些不是很成熟的设想,则被视为JavaScript.next.next,在更远的将来再考虑推出。TC39委员会的总体考虑是,ES5与ES3基本保持兼容,较大的语法修正和新功能加入,将由JavaScript.next完成。当时,JavaScript.next指的是ES6,第六版发布以后,就指ES7。TC39的判断是,ES5会在2021年的年中成为JavaScript开发的主流标准,并在此后五年中一直保持这个位置。2021年6月,ECMAscript 5.1版发布,并且成为ISO国际标准(ISO/IEC 16262:2021)。2021年3月,ECMAScript 6草案冻结,不再添加新功能。新的功能设想将被放到ECMAScript 7。2021年12月,ECMAScript 6草案发布。然后是12个月的讨论期,听取各方反馈。2021年6月,ECMAScript 6正式通过,成为国际标准。从2000年算起,这时已经过去了15年。部署进度各大浏览器的最新版本,对ES6的支持可以查看kangax.github.io/es5-compat-table/es6/。随着时间的推移,支持度已经越来越高了,ES6的大部分特性都实现了。Node.js是JavaScript语言的服务器运行环境,对ES6的支持度比浏览器更高。通过Node,可以体验更多ES6的特性。建议使用版本管理工具nvm,来安装Node,因为可以自由切换版本。不过,nvm不支持Windows系统,如果你使用Windows系统,下面的操作可以改用nvmw或nvm-windows代替。安装nvm需要打开命令行窗口,运行下面的命令。$ curl -o- https://raw.githubusercontent.com/creationix/nvm/<version number>/install.sh | bash上面命令的version number处,需要用版本号替换。本节写作时的版本号是v0.29.0。该命令运行后,nvm会默认安装在用户主目录的.nvm子目录。然后,激活nvm。$ source ~/.nvm/nvm.sh激活以后,安装Node的最新版。$ nvm install node安装完成后,切换到该版本。$ nvm use node使用下面的命令,可以查看Node所有已经实现的ES6特性。$ node --v8-options | grep harmony--harmony_typeof--harmony_scoping--harmony_modules--harmony_symbols--harmony_proxies--harmony_collections--harmony_observation--harmony_generators--harmony_iteration--harmony_numeric_literals--harmony_strings--harmony_arrays--harmony_maths--harmony上面命令的输出结果,会因为版本的不同而有所不同。我写了一个ES-Checker模块,用来检查各种运行环境对ES6的支持情况。访问ruanyf.github.io/es-checker,可以看到您的浏览器支持ES6的程度。运行下面的命令,可以查看你正在使用的Node环境对ES6的支持程度。$ npm install -g es-checker$ es-checker=========================================Passes 24 feature DectationsYour runtime supports 57% of ECMAScript 6=========================================Babel转码器Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。这意味着,你可以用ES6的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。// 转码前input.map(item => item + 1);// 转码后input.map(function (item) {return item + 1;});上面的原始代码用了箭头函数,这个特性还没有得到广泛支持,Babel将其转为普通函数,就能在现有的JavaScript环境执行了。配置文件.babelrcBabel的配置文件是.babelrc,存放在项目的根目录下。使用Babel的第一步,就是配置这个文件。该文件用来设置转码规则和插件,基本格式如下。{"presets": [],"plugins": []}presets字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。# ES2021转码规则$ npm install --save-dev babel-preset-es2021# react转码规则$ npm install --save-dev babel-preset-react# ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个$ npm install --save-dev babel-preset-stage-0$ npm install --save-dev babel-preset-stage-1$ npm install --save-dev babel-preset-stage-2$ npm install --save-dev babel-preset-stage-3然后,将这些规则加入.babelrc。{"presets": ["es2021","react","stage-2"],"plugins": []}注意,以下所有Babel工具和模块的使用,都必须先写好.babelrc。命令行转码babel-cliBabel提供babel-cli工具,用于命令行转码。它的安装命令如下。$ npm install --global babel-cli基本用法如下。# 转码结果输出到标准输出$ babel example.js# 转码结果写入一个文件# --out-file 或 -o 参数指定输出文件$ babel example.js --out-file compiled.js# 或者$ babel example.js -o compiled.js# 整个目录转码# --out-dir 或 -d 参数指定输出目录$ babel src --out-dir lib# 或者$ babel src -d lib# -s 参数生成source map文件$ babel src -d lib -s上面代码是在全局环境下,进行Babel转码。这意味着,如果项目要运行,全局环境必须有Babel,也就是说项目产生了对环境的依赖。另一方面,这样做也无法支持不同项目使用不同版本的Babel。一个解决办法是将babel-cli安装在项目之中。# 安装$ npm install --save-dev babel-cli然后,改写package.json。{// ..."devDependencies": {"babel-cli": "^6.0.0"},"scripts": {"build": "babel src -d lib"},}转码的时候,就执行下面的命令。$ npm run buildbabel-nodebabel-cli工具自带一个babel-node命令,提供一个支持ES6的REPL环境。它支持Node的REPL环境的所有功能,而且可以直接运行ES6代码。它不用单独安装,而是随babel-cli一起安装。然后,执行babel-node就进入REPL环境。$ babel-node> (x => x * 2)(1)2babel-node命令可以直接运行ES6脚本。将上面的代码放入脚本文件es6.js,然后直接运行。$ babel-node es6.js2babel-node也可以安装在项目中。$ npm install --save-dev babel-cli然后,改写package.json。{"scripts": {"script-name": "babel-node script.js"}}上面代码中,使用babel-node替代node,这样script.js本身就不用做任何转码处理。babel-registerbabel-register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码。$ npm install --save-dev babel-register使用时,必须首先加载babel-register。require("babel-register");require("./index.js");q
标签:

js es6异步操作和Async函数js代码

ES6 异步操作和async函数 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可。 ES6诞生以前,异步编ES6 异步操作和async函数异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可。ES6诞生以前,异步编程的方法,大概有下面四种。回调函数事件监听发布/订阅Promise 对象ES6将JavaScript异步编程带入了一个全新的阶段,ES7的Async函数更是提出了异步编程的终极解决方案。基本概念异步所谓"异步",简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。回调函数JavaScript语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它的英语名字callback,直译过来就是"重新调用"。读取文件进行处理,是这样写的。fs.readFile('/etc/passwd', function (err, data) {if (err) throw err;console.log(data);});上面代码中,readFile函数的第二个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行。一个有趣的问题是,为什么Node.js约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是null)?原因是执行分成两段,在这两段之间抛出的错误,程序无法捕捉,只能当作参数,传入第二段。Promise回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取A文件之后,再读取B文件,代码如下。fs.readFile(fileA, function (err, data) {fs.readFile(fileB, function (err, data) {// ...});});不难想象,如果依次读取多个文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。这种情况就称为"回调函数地狱"(callback hell)。Promise就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用Promise,连续读取多个文件,写法如下。var readFile = require('fs-readfile-promise');readFile(fileA).then(function(data){console.log(data.toString());}).then(function(){return readFile(fileB);}).then(function(data){console.log(data.toString());}).catch(function(err) {console.log(err);});上面代码中,我使用了fs-readfile-promise模块,它的作用就是返回一个Promise版本的readFile函数。Promise提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。可以看到,Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。那么,有没有更好的写法呢?Generator函数协程传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。协程有点像函数,又有点像线程。它的运行流程大致如下。第一步,协程A开始执行。第二步,协程A执行到一半,进入暂停,执行权转移到协程B。第三步,(一段时间后)协程B交还执行权。第四步,协程A恢复执行。上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。举例来说,读取文件的协程写法如下。function *asyncJob() {// ...其他代码var f = yield readFile(fileA);// ...其他代码}上面代码的函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。Generator函数的概念Generator函数是协程在ES6的实现,最大特点就是可以交出函数的执行权(即暂停执行)。整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。Generator函数的执行方法如下。function* gen(x){var y = yield x + 2;return y;}var g = gen(1);g.next() // { value: 3, done: false }g.next() // { value: undefined, done: true }上面代码中,调用Generator函数,会返回一个内部指针(即遍历器)g 。这是Generator函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g的next方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,上例是执行到x + 2为止。换言之,next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示Generator函数是否执行完毕,即是否还有下一个阶段。Generator函数的数据交换和错误处理Generator函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。next方法返回值的value属性,是Generator函数向外输出数据;next方法还可以接受参数,这是向Generator函数体内输入数据。function* gen(x){var y = yield x + 2;return y;}var g = gen(1);g.next() // { value: 3, done: false }g.next(2) // { value: 2, done: true }上面代码中,第一个next方法的value属性,返回表达式x + 2的值(3)。第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收。因此,这一步的 value 属性,返回的就是2(变量y的值)。Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。function* gen(x){try {var y = yield x + 2;} catch (e){console.log(e);}return y;}var g = gen(1);g.next();g.throw('出错了');// 出错了上面代码的最后一行,Generator函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try ...catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。异步任务的封装下面看看如何使用 Generator 函数,执行一个真实的异步任务。var fetch = require('node-fetch');function* gen(){var url = 'https://api.github.com/users/github';var result = yield fetch(url);console.log(result.bio);}上面代码中,Generator函数封装了一个异步操作,该操作先读取一个远程接口,然后从JSON格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。执行这段代码的方法如下。var g = gen();var result = g.next();result.value.then(function(data){return data.json();}).then(function(data){g.next(data);});上面代码中,首先执行Generator函数,获取遍历器对象,然后使用next 方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next 方法。可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。Thunk函数参数的求值策略Thunk函数早在上个世纪60年代就诞生了。那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是"求值策略",即函数的参数到底应该何时求值。var x = 1;function f(m){return m * 2;}f(x + 5)上面代码先定义函数f,然后向它传入表达式x + 5。请问,这个表达式应该何时求值?一种意见是"传值调用"(call by value),即在进入函数体之前,就计算x + 5的值(等于6),再将这个值传入函数f 。C语言就采用这种策略。f(x + 5)// 传值调用时,等同于f(6)另一种意见是"传名调用"(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。Haskell语言采用这种策略。f(x + 5)// 传名调用时,等同于(x + 5) * 2传值调用和传名调用,哪一种比较好?回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。function f(a, b){return b;}f(3 * x * x - 2 * x - 1, x);上面代码中,函数f的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。因此,有一些计算机学家倾向于"传名调用",即只在执行时求值。Thunk函数的含义编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。function f(m){return m * 2;}f(x + 5);// 等同于var thunk = function () {return x + 5;};function f(thunk){return thunk() * 2;}上面代码中,函数f的参数x + 5被一个函数替换了。凡是用到原参数的地方,对Thunk函数求值即可。这就是Thunk函数的定义,它是"传名调用"的一种实现策略,用来替换某个表达式。JavaScript语言的Thunk函数JavaScript语言是传值调用,它的Thunk函数含义有所不同。在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。// 正常版本的readFile(多参ٗ
标签:

js代码 es6参考链接,js参考链接

ES6 参考链接 官方文件ECMAScript® 2015 Language Specification: ECMAScript 2015规格 ECMAScript® 2016 LanES6 参考链接官方文件ECMAScript® 2021 Language Specification: ECMAScript 2021规格ECMAScript® 2021 Language Specification: ECMAScript 2021规格ECMAScript® 2021 Language Specification:ECMAScript 2021规格(草案)ECMAScript Current Proposals: ECMAScript当前的所有提案ECMAScript Active Proposals: 已经进入正式流程的提案ECMAscript proposals:从阶段0到阶段4的所有提案列表TC39 meeting agendas: TC39 委员会历年的会议记录ECMAScript Daily: TC39委员会的动态The TC39 Process: 提案进入正式规格的流程TC39: A Process Sketch, Stages 0 and 1: Stage 0 和 Stage 1 的含义综合介绍Axel Rauschmayer, Exploring ES6: Upgrade to the next version of JavaScript: ES6的专著,本书的许多代码实例来自该书Sayanee Basu, Use ECMAScript 6 TodayAriya Hidayat, Toward Modern Web Apps with ECMAScript 6Dale Schouten, 10 Ecmascript-6 tricks you can perform right nowColin Toh, Lightweight ES6 Features That Pack A Punch: ES6的一些“轻量级”的特性介绍Domenic Denicola, ES6: The Awesome PartsNicholas C. Zakas, Understanding ECMAScript 6Justin Drake, ECMAScript 6 in Node.JSRyan Dao, Summary of ECMAScript 6 major featuresLuke Hoban, ES6 features: ES6新语法点的罗列Traceur-compiler, Language Features: Traceur文档列出的一些ES6例子Axel Rauschmayer, ECMAScript 6: what‡s next for JavaScript?: 关于ES6新增语法的综合介绍,有很多例子Axel Rauschmayer, Getting started with ECMAScript 6: ES6语法点的综合介绍Toby Ho, ES6 in io.jsGuillermo Rauch, ECMAScript 6Charles King, The power of ECMAScript 6Benjamin De Cock, Frontend Guidelines: ES6最佳实践Jani Hartikainen, ES6: What are the benefits of the new features in practice?kangax, Javascript quiz. ES6 edition: ES6小测试Jeremy Fairbank, HTML5DevConf ES7 and Beyond!: ES7新增语法点介绍let和constKyle Simpson, For and against let: 讨论let命令的作用域kangax, Why typeof is no longer “safe”: 讨论在块级作用域内,let命令的变量声明和赋值的行为Axel Rauschmayer, Variables and scoping in ECMAScript 6: 讨论块级作用域与let和const的行为Nicolas Bevacqua, ES6 Let, Const and the “Temporal Dead Zone” (TDZ) in Depthacorn, Function statements in strict mode: 块级作用域对严格模式的函数声明的影响Axel Rauschmayer, ES proposal: global: 顶层对象global解构赋值Nick Fitzgerald, Destructuring Assignment in ECMAScript 6: 详细介绍解构赋值的用法Nicholas C. Zakas, ECMAScript 6 destructuring gotcha字符串Nicholas C. Zakas, A critical review of ECMAScript 6 quasi-literalsMozilla Developer Network, Template stringsAddy Osmani, Getting Literal With ES6 Template Strings: 模板字符串的介绍Blake Winton, ES6 Templates: 模板字符串的介绍Peter Jaszkowiak, How to write a template compiler in JavaScript: 使用模板字符串,编写一个模板编译函数Axel Rauschmayer, ES.stage3: string padding正则Mathias Bynens, Unicode-aware regular expressions in ES6: 详细介绍正则表达式的u修饰符Axel Rauschmayer, New regular expression features in ECMAScript 6:ES6正则特性的详细介绍Yang Guo, RegExp lookbehind assertions:介绍后行断言数值Nicolas Bevacqua, ES6 Number Improvements in Depth数组Axel Rauschmayer, ECMAScript 6‡s new array methods: 对ES6新增的数组方法的全面介绍TC39, Array.prototype.includes: 数组的includes方法的规格Axel Rauschmayer, ECMAScript 6: holes in Arrays: 数组的空位问题函数Nicholas C. Zakas, Understanding ECMAScript 6 arrow functionsJack Franklin, Real Life ES6 - Arrow FunctionsAxel Rauschmayer, Handling required parameters in ECMAScript 6Dmitry Soshnikov, ES6 Notes: Default values of parameters: 介绍参数的默认值Ragan Wald, Destructuring and Recursion in ES6: rest参数和扩展运算符的详细介绍Axel Rauschmayer, The names of functions in ES6: 函数的name属性的详细介绍Kyle Simpson, Arrow This: 箭头函数并没有自己的thisDerick Bailey, Do ES6 Arrow Functions Really Solve “this” In JavaScript?:使用箭头函数处理this指向,必须非常小心Mark McDonnell, Understanding recursion in functional JavaScript programming: 如何自己实现尾递归优化Nicholas C. Zakas, The ECMAScript 2021 change you probably don't know: 使用参数默认值时,不能在函数内部显式开启严格模式对象Addy Osmani, Data-binding Revolutions with Object.observe(): 介绍Object.observe()的概念Sella Rafaeli, Native JavaScript Data-Binding: 如何使用Object.observe方法,实现数据对象与DOM对象的双向绑定Axel Rauschmayer, __proto__ in ECMAScript 6Axel Rauschmayer, Enumerability in ECMAScript 6Axel Rauschmayer, ES proposal: Object.getOwnPropertyDescriptors()TC39, Object.getOwnPropertyDescriptors ProposalSymbolAxel Rauschmayer, Symbols in ECMAScript 6: Symbol简介MDN, Symbol: Symbol类型的详细介绍Jason Orendorff, ES6 In Depth: SymbolsKeith Cirkel, Metaprogramming in ES6: Symbols and why they're awesome: Symbol的深入介绍Axel Rauschmayer, Customizing ES6 via well-known symbolsDerick Bailey, Creating A True Singleton In Node.js, With ES6 SymbolsDas Surma, How to read web specs Part IIa ‡ Or: ECMAScript Symbols: 介绍 Symbol 的规格Set和MapMozilla Developer Network, WeakSet:介绍WeakSet数据结构Dwayne Charrington, What Are Weakmaps In ES6?: WeakMap数据结构介绍Axel Rauschmayer, ECMAScript 6: maps and sets: Set和Map结构的详细介绍Jason Orendorff, ES6 In Depth: Collections:Set和Map结构的设计思想Axel Rauschmayer, Converting ES6 Maps to and from JSON: 如何将Map与其他数据结构互相转换Proxy和ReflectNicholas C. Zakas, Creating defensive objects with ES6 proxiesAxel Rauschmayer, Meta programming with ECMAScript 6 proxies: Proxy详解Daniel Zautner, Meta-programming JavaScript Using Proxies: 使用Proxy实现元编程Tom Van Cutsem, Harmony-reflect: Reflect对象的设计目的Tom Van Cutsem, Proxy Traps: Proxy拦截操作一览Tom Van Cutsem, Reflect APITom Van Cutsem, Proxy Handler APINicolas Bevacqua, ES6 Proxies in DepthNicolas Bevacqua, ES6 Proxy Traps in DepthNicolas Bevacqua, More ES6 Proxy Traps in DepthAxel Rauschmayer, Pitfall: not all objects can be wrapped transparently by proxiesBertalan Miklos, Writing a JavaScript Framework - Data Binding with ES6 Proxies: 使用 Proxy 实现观察者模式Promise对象Jake Archibald, JavaScript Promises: There and back againTilde, rsvp.jsSandeep Panda, An Overview of JavaScript Promises: ES6 Promise入门介绍Dave Atchley, ES6 Promises: Promise的语法介绍Axel Rauschmayer, ECMAScript 6 promises (2/2): the API: 对ES6 Promise规格和用法的详细介绍Jack Franklin, Embracing Promises in JavaScript: catch 方法的例子Ronald Chen, How to escape Promise Hell: 如何使用Promise.all方法的一些很好的例子Jordan Harband, proposal-promise-try: Promise.try() 方法的提案Sven Slootweg, What is Promise.try, and why does it matter?: Promise.try() 方法的优点Yehuda Katz, TC39: Promises, Promises: Promise.try() 的用处IteratorMozilla Developer Network, Iterators and generatorsMozilla Developer Network, The Iterator protocolJason Orendorff, ES6 In Depth: Iterators and the for-of loop: 遍历器与for...of循环的介绍Axel Rauschmayer, Iterators and generators in ECMAScript 6: 探讨Iterator和Generator的设计目的Axel Rauschmayer, Iterables and iterators in ECMAScript 6: Iterator的详细介绍Kyle Simpson, Iterating ES6 Numbers: 在数值对象上部署遍历器GeneratorMatt Baker, Replacing callbacks with ES6 GeneratorsSteven Sanderson, Experiments with Koa and JavaScript Generatorsjmar777, What's the Big Deal with Generators?Marc Harter, Generators in Node.js: Common Misconceptions and Three Good Use Cases: 讨论Generator函数的作用StackOverflow, ES6 yield : what happens to the arguments of the first call next()?: 第一次使用next方法时不能带有参数Kyle Simpson, ES6 Generators: Complete Series: 由浅入深探讨Generator的系列文章,共四篇Gajus Kuizinas, The Definitive Guide to the JavaScript Generators: 对Generator的综合介绍Jan Krems, Generators Are Like Arrays: 讨论Generator可以被当作数据结构看待Harold Cooper, Coroutine Event Loops in Javascript: Generator用于实现状态机Ruslan Ismagilov, learn-generators: 编程练习,共6道题Steven Sanderson, Experiments with Koa and JavaScript Generators: Generator入门介绍,以Koa框架为例Mahdi Dibaiee, ES7 Array and Generator comprehensions:ES7的Generator推导Nicolas Bevacqua, ES6 Generators in DepthAxel Rauschmayer, ES6 generators in depth: Generator规格的详尽讲解Derick Bailey, Using ES6 Generators To Short-Circuit Hierarchical Data Iteration:使用 for...of 循环完成预定的操作步骤异步操作和Async函数Luke Hoban, Async Functions for ECMAScript: Async函数的设计思想,与Promise、Gernerator函数的关系Jafar Husain, Asynchronous Generators for ES7: Async函数的深入讨论Nolan Lawson, Taming the asynchronous beast with ES7: async函数通俗的实例讲解Jafar Husain, Async Generators: 对async与Generator混合使用的一些讨论Daniel Brain, Understand promises before you start using async/await: 讨论async/await与Promise的关系Jake Archibald, Async functions - making promises friendlyAxel Rauschmayer, ES proposal: asynchronous iteration: 异步遍历器的详细介绍ClassSebastian Porto, ES6 classes and JavaScript prototypes: ES6 Class的写法与ES5 Prototype的写法对比Jack Franklin, An introduction to ES6 classes: ES6 class的入门介绍Axel Rauschmayer, ECMAScript 6: new OOP features besides classesAxel Rauschmayer, Classes in ECMAScript 6 (final semantics): Class语法的详细介绍和设计思想分析Eric Faust, ES6 In Depth: Subclassing: Class语法的深入介绍Nicolás Bevacqua, Binding Methods to Class Instance Objects: 如何绑定类的实例中的thisDecoratorMaximiliano Fierro, Declarative vs Imperative: Decorators和Mixin介绍Justin Fagnani, "Real" Mixins with JavaScript Classes: 使用类的继承实现MixinAddy Osmani, Exploring ES2021 Decorators: Decorator的深入介绍Sebastian McKenzie, Allow decorators for functions as well: 为什么修饰器不能用于函数Maximiliano Fierro, Traits with ES7 Decorators: Trait的用法介绍Jonathan Creamer: Using ES2021 Decorators to Publish on an Event Bus: 使用修饰器实现自动发布事件ModuleJack Franklin, JavaScript Modules the ES6 Way: ES6模块入门Axel Rauschmayer, ECMAScript 6 modules: the final syntax: ES6模块的介绍,以及与CommonJS规格的详细比较Dave Herman, Static module resolution: ES6模块的静态化设计思想Jason Orendorff, ES6 In Depth: Modules: ES6模块设计思想的介绍Ben Newman, The Importance of import and export: ES6模块的设计思想ESDiscuss, Why is "export default var a = 1;" invalid syntax?二进制数组Ilmari Heikkinen, Typed Arrays: Binary Data in the BrowserKhronos, Typed Array SpecificationIan Elliot, Reading A BMP File In JavaScriptRenato Mangini, How to convert ArrayBuffer to and from StringAxel Rauschmayer, Typed Arrays in ECMAScript 6SIMDTC39, SIMD.js Stage 2MDN, SIMDTC39, ECMAScript SIMDAxel Rauschmayer, JavaScript gains support for SIMD工具Babel, Babel Handbook: Babel的用法介绍Google, traceur-compiler: Traceur编译器Casper Beyer, ECMAScript 6 Features and ToolsStoyan Stefanov, Writing ES6 today with jstransformES6 Module Loader, ES6 Module Loader Polyfill: 在浏览器和node.js加载ES6模块的一个库,文档里对ES6模块有详细解释Paul Miller, es6-shim: 一个针对老式浏览器,模拟ES6部分功能的垫片库(shim)army8735, Javascript Downcast: 国产的ES6到ES5的转码器esnext, ES6 Module Transpiler:基于node.js的将ES6模块转为ES5代码的命令行工具Sebastian McKenzie, BabelJS: ES6转译器SystemJS, SystemJS: 在浏览器中加载AMD、CJS、ES6模块的一个垫片库Modernizr, HTML5 Cross Browser Polyfills: ES6垫片库清单Facebook, regenerator: 将Generator函数转为ES5的转码器
标签:

js代码 es6Module,jsModule

ES6 module 历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的rES6 module历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。// CommonJS模块let { stat, exists, readFile } = require('fs');// 等同于let _fs = require('fs');let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile;上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取3个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。// ES6模块import { stat, exists, readFile } from 'fs';上面代码的实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。除了静态加载带来的各种好处,ES6 模块还有以下好处。不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。将来浏览器的新 API 就能用模块格式提供,不再必要做成全局变量或者navigator对象的属性。不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。严格模式ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。严格模式主要有以下限制。变量必须声明后再使用函数的参数不能有同名属性,否则报错不能使用with语句不能对只读属性赋值,否则报错不能使用前缀0表示八进制数,否则报错不能删除不可删除的属性,否则报错不能删除变量delete prop,会报错,只能删除属性delete global[prop]eval不会在它的外层作用域引入变量eval和arguments不能被重新赋值arguments不会自动反映函数参数的变化不能使用arguments.callee不能使用arguments.caller禁止this指向全局对象不能使用fn.caller和fn.arguments获取函数调用的堆栈增加了保留字(比如protected、static和interface)上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。export 命令模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。// profile.jsexport var firstName = 'Michael';export var lastName = 'Jackson';export var year = 1958;上面代码是profile.js文件,保存了用户信息。ES6将其视为一个模块,里面用export命令对外部输出了三个变量。export的写法,除了像上面这样,还有另外一种。// profile.jsvar firstName = 'Michael';var lastName = 'Jackson';var year = 1958;export {firstName, lastName, year};上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。export命令除了输出变量,还可以输出函数或类(class)。export function multiply(x, y) {return x * y;};上面代码对外输出一个函数multiply。通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。function v1() { ... }function v2() { ... }export {v1 as streamV1,v2 as streamV2,v2 as streamLatestVersion};上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。// 报错export 1;// 报错var m = 1;export m;上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出1,第二种写法通过变量m,还是直接输出1。1只是一个值,不是接口。正确的写法是下面这样。// 写法一export var m = 1;// 写法二var m = 1;export {m};// 写法三var n = 1;export {n as m};上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。同样的,function和class的输出,也必须遵守这样的写法。// 报错function f() {}export f;// 正确export function f() {};// 正确function f() {}export {f};另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。export var foo = 'bar';setTimeout(() => foo = 'baz', 500);上面代码输出变量foo,值为bar,500毫秒之后变成baz。这一点与CommonJS规范完全不同。CommonJS模块输出的是值的缓存,不存在动态更新,详见下文《ES6模块加载的实质》一节。最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。function foo() {export default 'bar' // SyntaxError}foo()上面代码中,export语句放在函数之中,结果报错。import 命令使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。// main.jsimport {firstName, lastName, year} from './profile';function setName(element) {element.textContent = firstName + ' ' + lastName;}上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。import { lastName as surname } from './profile';import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。import {myMethod} from 'util';上面代码中,util是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。foo();import { foo } from 'my_module';上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。// 报错import { 'f' + 'oo' } from 'my_module';// 报错let module = 'my_module';import { foo } from module;// 报错if (x === 1) {import { foo } from 'module1';} else {import { foo } from 'module2';}上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是没法得到值的。最后,import语句会执行所加载的模块,因此可以有下面的写法。import 'lodash';上面代码仅仅执行lodash模块,但是不输入任何值。如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。import 'lodash';import 'lodash';上面代码加载了两次lodash,但是只会执行一次。import { foo } from 'my_module';import { bar } from 'my_module';// 等同于import { foo, bar } from 'my_module';上面代码中,虽然foo和bar在两个语句中加载,但是它们对应的是同一个my_module实例。也就是说,import语句是 Singleton 模式。模块的整体加载除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。下面是一个circle.js文件,它输出两个方法area和circumference。// circle.jsexport function area(radius) {return Math.PI * radius * radius;}export function circumference(radius) {return 2 * Math.PI * radius;}现在,加载这个模块。// main.jsimport { area, circumference } from './circle';console.log('圆面积:' + area(4));console.log(

js代码 es6变量的解构赋值,js变量的解构赋值

ES6 变量的解构赋值 数组的解构赋值 基本用法 ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。 以前,为变量赋值,只能直接指定值。ES6 变量的解构赋值数组的解构赋值基本用法ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。以前,为变量赋值,只能直接指定值。var a = 1;var b = 2;var c = 3;ES6允许写成下面这样。var [a, b, c] = [1, 2, 3];上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。let [foo, [[bar], baz]] = [1, [[2], 3]];foo // 1bar // 2baz // 3let [ , , third] = ["foo", "bar", "baz"];third // "baz"let [x, , y] = [1, 2, 3];x // 1y // 3let [head, ...tail] = [1, 2, 3, 4];head // 1tail // [2, 3, 4]let [x, y, ...z] = ['a'];x // "a"y // undefinedz // []如果解构不成功,变量的值就等于undefined。var [foo] = [];var [bar, foo] = [1];以上两种情况都属于解构不成功,foo的值都会等于undefined。另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。let [x, y] = [1, 2, 3];x // 1y // 2let [a, [b], d] = [1, [2, 3], 4];a // 1b // 2d // 4上面两个例子,都属于不完全解构,但是可以成功。如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。// 报错let [foo] = 1;let [foo] = false;let [foo] = NaN;let [foo] = undefined;let [foo] = null;let [foo] = {};上面的表达式都会报错,因为等号右边的值,要么转为对象以后不具备Iterator接口(前五个表达式),要么本身就不具备Iterator接口(最后一个表达式)。解构赋值不仅适用于var命令,也适用于let和const命令。var [v1, v2, ..., vN ] = array;let [v1, v2, ..., vN ] = array;const [v1, v2, ..., vN ] = array;对于Set结构,也可以使用数组的解构赋值。let [x, y, z] = new Set(["a", "b", "c"]);x // "a"事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。function* fibs() {var a = 0;var b = 1;while (true) {yield a;[a, b] = [b, a + b];}}var [first, second, third, fourth, fifth, sixth] = fibs();sixth // 5上面代码中,fibs是一个Generator函数,原生具有Iterator接口。解构赋值会依次从这个接口获取值。默认值解构赋值允许指定默认值。var [foo = true] = [];foo // true[x, y = 'b'] = ['a']; // x='a', y='b'[x, y = 'b'] = ['a', undefined]; // x='a', y='b'注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。var [x = 1] = [undefined];x // 1var [x = 1] = [null];x // null上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。function f() {console.log('aaa');}let [x = f()] = [1];上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。let x;if ([1][0] === undefined) {x = f();} else {x = [1][0];}默认值可以引用解构赋值的其他变量,但该变量必须已经声明。let [x = 1, y = x] = [];// x=1; y=1let [x = 1, y = x] = [2];// x=2; y=2let [x = 1, y = x] = [1, 2]; // x=1; y=2let [x = y, y = 1] = [];// ReferenceError上面最后一个表达式之所以会报错,是因为x用到默认值y时,y还没有声明。对象的解构赋值解构不仅可以用于数组,还可以用于对象。var { foo, bar } = { foo: "aaa", bar: "bbb" };foo // "aaa"bar // "bbb"对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。var { bar, foo } = { foo: "aaa", bar: "bbb" };foo // "aaa"bar // "bbb"var { baz } = { foo: "aaa", bar: "bbb" };baz // undefined上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。如果变量名与属性名不一致,必须写成下面这样。var { foo: baz } = { foo: 'aaa', bar: 'bbb' };baz // "aaa"let obj = { first: 'hello', last: 'world' };let { first: f, last: l } = obj;f // 'hello'l // 'world'这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。var { foo: baz } = { foo: "aaa", bar: "bbb" };baz // "aaa"foo // error: foo is not defined上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。注意,采用这种写法时,变量的声明和赋值是一体的。对于let和const来说,变量不能重新声明,所以一旦赋值的变量以前声明过,就会报错。let foo;let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"let baz;let {bar: baz} = {bar: 1}; // SyntaxError: Duplicate declaration "baz"上面代码中,解构赋值的变量都会重新声明,所以报错了。不过,因为var命令允许重新声明,所以这个错误只会在使用let和const命令时出现。如果没有第二个let命令,上面的代码就不会报错。let foo;({foo} = {foo: 1}); // 成功let baz;({bar: baz} = {bar: 1}); // 成功上面代码中,let命令下面一行的圆括号是必须的,否则会报错。因为解析器会将起首的大括号,理解成一个代码块,而不是赋值语句。和数组一样,解构也可以用于嵌套结构的对象。var obj = {p: ['Hello',{ y: 'World' }]};var { p: [x, { y }] } = obj;x // "Hello"y // "World"注意,这时p是模式,不是变量,因此不会被赋值。var node = {loc: {start: {line: 1,column: 5}}};var { loc: { start: { line }} } = node;line // 1loc// error: loc is undefinedstart // error: start is undefined上面代码中,只有line是变量,loc和start都是模式,不会被赋值。下面是嵌套赋值的例子。let obj = {};let arr = [];({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });obj // {prop:123}arr // [true]对象的解构也可以指定默认值。var {x = 3} = {};x // 3var {x, y = 5} = {x: 1};x // 1y // 5var {x:y = 3} = {};y // 3var {x:y = 3} = {x: 5};y // 5var { message: msg = 'Something went wrong' } = {};msg // "Something went wrong"默认值生效的条件是,对象的属性值严格等于undefined。var {x = 3} = {x: undefined};x // 3var {x = 3} = {x: null};x // null上面代码中,如果x属性等于null,就不严格相等于undefined,导致默认值不会生效。如果解构失败,变量的值等于undefined。var {foo} = {bar: 'baz'};foo // undefined如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。// 报错var {foo: {bar}} = {baz: 'baz'};上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错,请看下面的代码。var _tmp = {baz: 'baz'};_tmp.foo.bar // 报错如果要将一个已经声明的变量用于解构赋值,必须非常小心。// 错误的写法var x;{x} = {x: 1};// SyntaxError: syntax error上面代码的写法会报错,因为JavaScript引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。// 正确的写法({x} = {x: 1});上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。解构赋值允许,等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。({} = [true, false]);({} = 'abc');({} = []);上面的表达式虽然毫无意义,但是语法是合法的,可以执行。对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。let { log, sin, cos } = Math;上面代码将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。var arr = [1, 2, 3];var {0 : first, [arr.length - 1] : last} = arr;first // 1last // 3上面代码对数组进行对象解构。数组arr的0键对应的值是1,[arr.length - 1]就是2键,对应的值是3。方括号这种写法,属于“属性名表达式”,参见《对象的扩展》一章。字符串的解构赋值字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。const [a, b, c, d, e] = 'hello';a

js代码 es6Symbol,jsSymbol

ES6 symbol 概述 ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin模式),新方法的名字就有可能与现有方ES6 symbol概述ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。Symbol值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。let s = Symbol();typeof s// "symbol"上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是Symbol数据类型,而不是字符串之类的其他类型。注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。var s1 = Symbol('foo');var s2 = Symbol('bar');s1 // Symbol(foo)s2 // Symbol(bar)s1.toString() // "Symbol(foo)"s2.toString() // "Symbol(bar)"上面代码中,s1和s2是两个Symbol值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。const obj = {toString() {return 'abc';}};const sym = Symbol(obj);sym // Symbol(abc)注意,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。// 没有参数的情况var s1 = Symbol();var s2 = Symbol();s1 === s2 // false// 有参数的情况var s1 = Symbol('foo');var s2 = Symbol('foo');s1 === s2 // false上面代码中,s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。Symbol值不能与其他类型的值进行运算,会报错。var sym = Symbol('My symbol');"your symbol is " + sym// TypeError: can't convert symbol to string`your symbol is ${sym}`// TypeError: can't convert symbol to string但是,Symbol值可以显式转为字符串。var sym = Symbol('My symbol');String(sym) // 'Symbol(My symbol)'sym.toString() // 'Symbol(My symbol)'另外,Symbol值也可以转为布尔值,但是不能转为数值。var sym = Symbol();Boolean(sym) // true!sym// falseif (sym) {// ...}Number(sym) // TypeErrorsym + 2 // TypeError作为属性名的Symbol由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。var mySymbol = Symbol();// 第一种写法var a = {};a[mySymbol] = 'Hello!';// 第二种写法var a = {[mySymbol]: 'Hello!'};// 第三种写法var a = {};Object.defineProperty(a, mySymbol, { value: 'Hello!' });// 以上写法都得到同样结果a[mySymbol] // "Hello!"上面代码通过方括号结构和Object.defineProperty,将对象的属性名指定为一个Symbol值。注意,Symbol值作为对象属性名时,不能用点运算符。var mySymbol = Symbol();var a = {};a.mySymbol = 'Hello!';a[mySymbol] // undefineda['mySymbol'] // "Hello!"上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个Symbol值。同理,在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中。let s = Symbol();let obj = {[s]: function (arg) { ... }};obj[s](123);上面代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个Symbol值。采用增强的对象写法,上面代码的obj对象可以写得更简洁一些。let obj = {[s](arg) { ... }};Symbol类型还可以用于定义一组常量,保证这组常量的值都是不相等的。log.levels = {DEBUG: Symbol('debug'),INFO: Symbol('info'),WARN: Symbol('warn')};log(log.levels.DEBUG, 'debug message');log(log.levels.INFO, 'info message');下面是另外一个例子。const COLOR_RED= Symbol();const COLOR_GREEN= Symbol();function getComplement(color) {switch (color) {case COLOR_RED:return COLOR_GREEN;case COLOR_GREEN:return COLOR_RED;default:throw new Error('Undefined color');}}常量使用Symbol值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。还有一点需要注意,Symbol值作为属性名时,该属性还是公开属性,不是私有属性。实例:消除魔术字符串魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,该由含义清晰的变量代替。function getArea(shape, options) {var area = 0;switch (shape) {case 'Triangle': // 魔术字符串area = .5 * options.width * options.height;break;/* ... more code ... */}return area;}getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串上面代码中,字符串“Triangle”就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。常用的消除魔术字符串的方法,就是把它写成一个变量。var shapeType = {triangle: 'Triangle'};function getArea(shape, options) {var area = 0;switch (shape) {case shapeType.triangle:area = .5 * options.width * options.height;break;}return area;}getArea(shapeType.triangle, { width: 100, height: 100 });上面代码中,我们把“Triangle”写成shapeType对象的triangle属性,这样就消除了强耦合。如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用Symbol值。const shapeType = {triangle: Symbol()};上面代码中,除了将shapeType.triangle的值设为一个Symbol,其他地方都不用修改。属性名的遍历Symbol 作为属性名,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。var obj = {};var a = Symbol('a');var b = Symbol('b');obj[a] = 'Hello';obj[b] = 'World';var objectSymbols = Object.getOwnPropertySymbols(obj);objectSymbols// [Symbol(a), Symbol(b)]下面是另一个例子,Object.getOwnPropertySymbols方法与for...in循环、Object.getOwnPropertyNames方法进行对比的例子。var obj = {};var foo = Symbol("foo");Object.defineProperty(obj, foo, {value: "foobar",});for (var i in obj) {console.log(i); // 无输出}Object.getOwnPropertyNames(obj)// []Object.getOwnPropertySymbols(obj)// [Symbol(foo)]上面代码中,使用Object.getOwnPropertyNames方法得不到Symbol属性名,需要使用Object.getOwnPropertySymbols方法。另一个新的API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。let obj = {[Symbol('my_key')]: 1,enum: 2,nonEnum: 3};Reflect.ownKeys(obj)// [Symbol(my_key), 'enum', 'nonEnum']由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。var size = Symbol('size');class Collection {constructor() {this[size] = 0;}add(item) {this[this[size]] = item;this[size]++;}static sizeOf(instance) {return instance[size];}}var x = new Collection();Collection.sizeOf(x) // 0x.add('foo');Collection.sizeOf(x) // 1Object.keys(x) // ['0']Object.getOwnPropertyNames(x) // ['0']Object.getOwnPropertySymbols(x) // [Symbol(size)]上面代码中,对象x的size属性是一个 Symbol 值,所以Object.keys(x)、Object.getOwnPropertyNames(x)都无法获取它。这就造成了一种非私有的内部方法的效果。Symbol.for(),Symbol.keyFor()有时,我们希望重新使用同一个Symbol值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。var s1 = Symbol.for('foo');var s2 = Symbol.for('foo');s1 === s2 // true上面代码中,s1和s2都是 Symbol 值,但是它们都是同样参数的Symbol.for方法生成的,所以实际上是同一个值。Symbol.for()与Symbol()这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30次,会返回30个不同的Symbol值。Symbol.for("bar") === Symbol.for("bar")// trueSymbol("bar") === Symbol("bar")// false上面代码中,由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。var s1 = Symbol.for("foo");Symbol.keyFor(s1) // "foo"var s2 = Symbol("foo");Symbol.keyFor(s2) // undefined

js es6let和const命令js代码

ES6 let和const命令 let命令 基本用法 ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。 {let a =ES6 let和const命令let命令基本用法ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。{let a = 10;var b = 1;}a // ReferenceError: a is not defined.b // 1上面代码在代码块之中,分别用let和var声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。for循环的计数器,就很合适使用let命令。for (let i = 0; i < 10; i++) {}console.log(i);//ReferenceError: i is not defined上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。下面的代码如果使用var,最后输出的是10。var a = [];for (var i = 0; i < 10; i++) {a[i] = function () {console.log(i);};}a[6](); // 10上面代码中,变量i是var声明的,在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。var a = [];for (let i = 0; i < 10; i++) {a[i] = function () {console.log(i);};}a[6](); // 6上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。不存在变量提升let不像var那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则报错。console.log(foo); // 输出undefinedconsole.log(bar); // 报错ReferenceErrorvar foo = 2;let bar = 2;上面代码中,变量foo用var命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量bar用let命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。暂时性死区只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。var tmp = 123;if (true) {tmp = 'abc'; // ReferenceErrorlet tmp;}上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。if (true) {// TDZ开始tmp = 'abc'; // ReferenceErrorconsole.log(tmp); // ReferenceErrorlet tmp; // TDZ结束console.log(tmp); // undefinedtmp = 123;console.log(tmp); // 123}上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。typeof x; // ReferenceErrorlet x;上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。typeof undeclared_variable // "undefined"上面代码中,undeclared_variable是一个不存在的变量名,结果返回“undefined”。所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。有些“死区”比较隐蔽,不太容易发现。function bar(x = y, y = 2) {return [x, y];}bar(); // 报错上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。如果y的默认值是x,就不会报错,因为此时x已经声明了。function bar(x = 2, y = x) {return [x, y];}bar(); // [2, 2]ES6规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在ES5是很常见的,现在有了这种规定,避免此类错误就很容易了。总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。不允许重复声明let不允许在相同作用域内,重复声明同一个变量。// 报错function () {let a = 10;var a = 1;}// 报错function () {let a = 10;let a = 1;}因此,不能在函数内部重新声明参数。function func(arg) {let arg; // 报错}function func(arg) {{let arg; // 不报错}}块级作用域为什么需要块级作用域?ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。第一种场景,内层变量可能会覆盖外层变量。var tmp = new Date();function f() {console.log(tmp);if (false) {var tmp = "hello world";}}f(); // undefined上面代码中,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。第二种场景,用来计数的循环变量泄露为全局变量。var s = 'hello';for (var i = 0; i < s.length; i++) {console.log(s[i]);}console.log(i); // 5上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。ES6的块级作用域let实际上为JavaScript新增了块级作用域。function f1() {let n = 5;if (true) {let n = 10;}console.log(n); // 5}上面的函数有两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n,最后输出的值就是10。ES6允许块级作用域的任意嵌套。{{{{{let insane = 'Hello World'}}}}};上面代码使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。{{{{{let insane = 'Hello World'}console.log(insane); // 报错}}}};内层作用域可以定义外层作用域的同名变量。{{{{let insane = 'Hello World';{let insane = 'Hello World'}}}}};块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。// IIFE 写法(function () {var tmp = ...;...}());// 块级作用域写法{let tmp = ...;...}块级作用域与函数声明函数能不能在块级作用域之中声明,是一个相当令人混淆的问题。ES5规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。// 情况一if (true) {function f() {}}// 情况二try {function f() {}} catch(e) {}上面代码的两种函数声明,根据ES5的规定都是非法的。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。不过,“严格模式”下还是会报错。// ES5严格模式'use strict';if (true) {function f() {}}// 报错ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。// ES6严格模式'use strict';if (true) {function f() {}}// 不报错ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。function f() { console.log('I am outside!'); }(function () {if (false) {// 重复声明一次函数ffunction f() { console.log('I am inside!'); }}f();}());上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。// ES5版本function f() { console.log('I am outside!'); }(function () {function f() { console.log('I am inside!'); }if (false) {}f();}());ES6 的运行结果就完全不一样了,会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响,实际运行的代码如下。// ES6版本function f() { console.log('I am outside!'); }(function () {f();}());很显然,这种行为差异会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6在附录B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。允许在块级作用域内声明函数。函数声明类似于var,即会提升到全局作用域或函数作用域的头部。同时,函数声明还会提升到所在的块级作用域的头部。注意,上面三条规则只对ES6的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。前面那段代码,在 Chrome 环境Ӡ
标签: