前言
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 (ES2015)年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
1.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)。日常场合,这两个词是可以互换的。
2.ECMAScript 的历史
- ES6 从开始制定到最后发布,整整用了 15 年
- 2000 年,ECMAScript 4.0 开始酝酿
- 2007 年 10 月,ECMAScript 4.0 版草案发布
- 2009 年 12 月,ECMAScript 5.0 版正式发布
- 2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。
- 2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。
- 2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。
- 2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。
- 目前,各大浏览器对 ES6 的支持可以查看kangax.github.io/compat-table/es6/。
3.Babel 转码器
Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。(知道就行,后面webpack课程会讲到)
// 转码前 es6 箭头函数
input.map(item => item + 1);
// 转码后 es5
input.map(function (item) {
return item + 1;
});
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
。
for(var i=0;i<10;i++){}
console.log(i); //10
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代码中,变量i
是var
命令声明的,在全局范围内都有效,所以全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
。也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是 10。
如果使用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
。你可能会问,如果每一轮循环的变量i
都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i
时,就在上一轮循环的基础上进行计算。
另外,for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。(作用域相互独立)
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次abc
。这表明函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域。
不存在变量提升
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
上面代码中,变量foo
用var
命令声明,会发生变量提升,即脚本开始运行时,变量foo
已经存在了,但是没有值,所以会输出undefined
。变量bar
用let
命令声明,不会发生变量提升。这表示在声明它之前,变量bar
是不存在的,这时如果用到它,就会抛出一个错误。
暂时性死区
只要块级作用域内存在let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域,所以在let
声明变量前,对tmp
赋值会报错。
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
上面代码中,在let
命令声明变量tmp
之前,都属于变量tmp
的“死区”。
“暂时性死区”也意味着typeof
不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x;
上面代码中,变量x
使用let
命令声明,所以在声明之前,都属于x
的“死区”,只要用到该变量就会报错。因此,typeof
运行时就会抛出一个ReferenceError
。
作为比较,如果一个变量根本没有被声明,使用typeof
反而不会报错。
console.log(typeof a);// "undefined"
var a = 10;
上面代码中,typeof a 结果返回“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]
另外,下面的代码也会报错,与var
的行为不同。
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
上面代码报错,也是因为暂时性死区。使用let
声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x
的声明语句还没有执行完成前,就去取x
的值,导致报错”x 未定义“。
ES6 规定暂时性死区和let
、const
语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
不允许重复声明
let
不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
因此,不能在函数内部重新声明参数。
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
块级作用域
为什么需要块级作用域?
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
上面代码的原意是,if
代码块的外部使用外层的tmp
变量,内部使用内层的tmp
变量。但是,函数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'}
console.log(insane); // 报错
}}}};
上面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量。
内层作用域可以定义外层作用域的同名变量。
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
;(function () {
var tmp = ...;
...
}());
IIFE的好处:
1.避免作用域命名污染
2.提升性能(减少了对作用域的查找)
3.避免全局命名冲突
4.保存闭包状态
// 块级作用域写法
{
let tmp = ...;
...
}
块级作用域与函数声明
函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
// 情况一
if (true) {
function f() {}
}
// 情况二
try {
function f() {}
} catch(e) {
// ...
}
上面两种函数声明,根据 ES5 的规定都是非法的。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let
,在块级作用域之外不可引用。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function 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 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
上面的代码在 ES6 浏览器中,都会报错。
原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
- 允许在块级作用域内声明函数。
- 函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 - 同时,函数声明还会提升到所在的块级作用域的头部。
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let
处理。
根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var
声明的变量。上面的例子实际运行的代码如下。
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'secret';
function f() {
return a;
}
}
// 块级作用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
上面代码中,第一种写法没有大括号,所以不存在块级作用域,而let
只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。
const 命令
基本用法
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
上面代码表明改变常量的值会报错。
const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
const foo;
// SyntaxError: Missing initializer in const declaration
上面代码表示,对于const
来说,只声明不赋值,就会报错。
const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
上面代码在常量MAX
声明之前就调用,结果报错。
const
声明的常量,也与let
一样不可重复声明。
var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
上面代码中,常量foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
下面是另一个例子。
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
上面代码中,常量a
是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a
,就会报错。
如果真的想将对象冻结,应该使用Object.freeze
方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
上面代码中,常量foo
指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,后面章节还会提到,另外两种声明变量的方法:import
命令和class
命令。所以,ES6 一共有 6 种声明变量的方法。
顶层对象的属性
顶层对象,在浏览器环境指的是window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window
对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
上面代码中,全局变量a
由var
命令声明,所以它是顶层对象的属性;全局变量b
由let
命令声明,所以它不是顶层对象的属性,返回undefined
。
globalThis 对象
JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。
- 浏览器里面,顶层对象是
window
,但 Node 和 Web Worker 没有window
。 - 浏览器和 Web Worker 里面,
self
也指向顶层对象,但是 Node 没有self
。 - Node 里面,顶层对象是
global
,但其他环境都不支持。
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this
变量,但是有局限性。
- 全局环境中,
this
会返回顶层对象。但是,Node.js 模块中this
返回的是当前模块,ES6 模块中this
返回的是undefined
。 - 函数里面的
this
,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this
会指向顶层对象。但是,严格模式下,这时this
会返回undefined
。 - 不管是严格模式,还是普通模式,
new Function('return this')()
,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval
、new Function
这些方法都可能无法使用。
综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。
ES2020 在语言标准的层面,引入globalThis
作为顶层对象。也就是说,任何环境下,globalThis
都是存在的,都可以从它拿到顶层对象,指向全局环境下的this
。
demo.js
浏览器环境:
console.log(globalThis); //window
node环境
console.log(globalThis); //global
变量的解构赋值
概述
解构赋值是对赋值运算符的扩展。他是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。在代码书写上简洁且易读,语义更加清晰明了;也方便了复杂对象中数据字段获取。
- 数组的解构赋值
- 对象的解构赋值
- 字符串的解构赋值
- 数值和布尔值的解构赋值
- 函数参数的解构赋值
- 圆括号问题
- 用途
数组的解构赋值
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。以前,为变量赋值,只能直接指定值。
let a = 1;
let b = 2;
let c = 3;
ES6 允许写成下面这样。
let [a, b, c] = [1, 2, 3];
数组解构赋值:就是等号左右两边按照位置对应关系,从数组中提取值,对变量进行赋值的操作。
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
案例分析:
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
如果解构不成功,变量的值就等于undefined
。
let [foo] = [];
let [bar, foo] = [1];
不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
Iterator接口:是一个遍历器对象,为各种不同的数据结构提供统一的访问机制
原生具备 Iterator 接口的数据结构如下。
Array
Map
Set
String
TypedArray
函数的 arguments 对象
NodeList 对象
默认值
解构赋值允许指定默认值。
```js
let [foo = true] = [];
foo // truelet [x, y = ‘b’] = [‘a’]; // x=’a’, y=’b’
let [x, y = ‘b’] = [‘a’, undefined]; // x=’a’, y=’b’- 注意,ES6 内部使用严格相等运算符(`===`),判断一个位置是否有值。所以,只有当一个数组成员严格等于`undefined`,默认值才会生效。 - ```js let [x = 1] = [undefined]; x // 1 let [x = 1] = [null]; x // null
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
```js
function f() {
console.log(‘aaa’);
}let [x = f()] = [1];
### 三点运算符 ```js 符号:... 根据用法:又可称之为 1.展开运算符 //把a数组中的数据,展开,放入到arr中,就可以使用 ... 运算符 let a = [1,2,3,4,5,6]; let arr = ['aa','bb','cc',...a] arr //['aa','bb','cc',1,2,3,4,5,6] 2.剩余运算符 //按照数组位置对应关系,进行赋值,把剩余的数据,打包以数组形式,赋值给d let [a,b,c,...d] = [1,2,3,4,5,6,7,8,9,10]; a //1 b //2 c //3 d //[4,5,6,7,8,9,10]
对象的解构赋值
基本用法
解构不仅可以用于数组,还可以用于对象。对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
数组的解构是按照位置对应关系赋值,对象的解构赋值是按照键值对的对应关系进行赋值
let {name:myname,age:myage,sex:mysex} = {name:'lisi',age:25}
myname //lisi
myage //25
mysex //undefined
对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。
let {log:log,dir:dir} = console;
//把console对象的方法log(控制台输出信息) /dir(展示一个对象下的所有的属性和方法) 通过解构赋值给 log 和dir变量
console.log('xxx'); 等价于 log('xxx');
console.dir(window); 等价于 dir(window);
let ? = Math; ?部分怎么写
对象的解构赋值可以是下面形式的简写(参见《对象的扩展》一章)
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
与数组一样,解构也可以用于嵌套结构的对象
let obj = {
list:['aa','bb','cc'],
hobby:{
ball:['篮球','足球','乒乓球'],
fruits:['榴莲','百香果','杨桃']
}
}
let {list,hobby,list:[a,b,c]} = obj
list //['aa','bb','cc']
hobby //{ball:['篮球','足球','乒乓球'],fruits:['榴莲','百香果','杨桃']}
a //aa
b //bb
c // cc
默认值
对象的解构也可以指定默认值,默认值生效的条件是,对象的属性值严格等于undefined
。
let {x,y=20} = {x:10}
x//10
y//20
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
注意点
(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
//right
let {x} = {x:1}
上面代码的写法会报错,因为 JavaScript 引擎会将{x}
理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
// 正确的写法
let x;
({x} = {x: 1});
let x;
....
....
....
({x} = obj);
上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。
(2)解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
({} = [true, false]);
({} = 'abc');
({} = []);
上面的表达式虽然毫无意义,但是语法是合法的,可以执行。
(3)由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
let arr = [1, 2, 3]; 等价于 let arr = {0:1, 1:2, 2:3}
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
上面代码对数组进行对象解构。数组arr
的0
键对应的值是1
,[arr.length - 1]
就是2
键,对应的值是3
。方括号这种写法,属于“属性名表达式”(参见《对象的扩展》一章)。
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello'; 'hello' == ['h','e','l','l','0']
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length
属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
//解析 'hello'
let hello = {
length:5
}
let {length:len} = hello;
数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
上面代码中,数值和布尔值的包装对象都有toString
属性,因此变量s
都能取到值。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数参数的解构赋值
函数的参数也可以使用解构赋值。
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
等价写法?
函数参数的解构也可以使用默认值。
function add([x, y=10]){
return x + y;
}
add([1]); // 11
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
圆括号问题
解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。
由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。
let {(x)} = {x:1}
用途
变量的解构赋值用途很多。
(1)交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
上面代码交换变量x
和y
的值,这样的写法不仅简洁,而且易读,语义非常清晰。
(2)从函数返回多个值
函数能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取 JSON 数据
解构赋值对提取 JSON 对象中的数据,尤其有用。
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
上面代码可以快速提取 JSON 数据的值。
(5)函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';
这样的语句。
(6)遍历 Map 结构
任何部署了 Iterator 接口的对象,都可以用for...of
循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
如果只想获取键名,或者只想获取键值,可以写成下面这样。
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
(7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");
//加载进来的也是一个对象,本质,就是对象的解构赋值
函数扩展&模板字符串
目标
- 模版字符串
- 函数参数 的默认值
- rest参数
- 箭头函数
模版字符串
在传统javascript中,输出模版通常是字符串拼接的形式,如下:
let person = {
name:'铁柱',
age:45,
job:'不会写代码的铁匠不是一个合格的干饭人,请问我的工作是什么'
}
let str = '<ul><li>我叫:'+person.name+'</li></li>我今年:'+person.age+'</li><li>我的工作:'+person.job+'<li></ul>';
缺点:写法繁琐,不好维护,使用不方便;
改进方案:ES6中,引入模版字符串,来解决此问题;
ES6中用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量
//普通自字符串
`javaScript String`
//多行字符串
`It's a fine day today
Let's study together
`
//在字符串中嵌入变量,需要将变量名写在${}之中
`
<ul>
<li>我叫:${person.name}</li>
<li>我今年:${person.age}</li>
<li>我的工作:${person.job}</li>
</ul>
`
//大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
//模板字符串之中还能调用函数
`今天是:${new Date()}`
let x = 1;
let y = 2;
`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"
函数参数的默认值
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function add(a=1,b=1){
return a+b;
}
add();//2
add(2);//3
add(20,1) //21
ES6之前函数参数默认值的实现:
function add(a,b){
return a+b;
}
add();//NaN
function add(a,b){
a = a || 1;
b = b || 1;
return a+b;
}
add();//2
//传参,但是传参时对应的布尔值为false,则赋值不起作用
add(2,0) // 3 原因是0在做 || 运算时,被对应转成了false, 所以默认值 1 起作用了。 可以尝试 '' ,undefined,null 等
优化:
function add(a,b){
a = typeof a=='undefined'?1:a;
b = typeof b=='undefined'?1:b;
return a+b;
}
add();//2
add(2)//3
add(2,undefined); //3 依然存在问题,所以推荐使用ES6中的函数参数默认值
与解构赋值默认值结合使用(回顾上一章节)
rest参数
ES6 引入 rest 参数(形式为...变量名
),用于获取函数的多余参数(…运算符,剩余运算符的用法),这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...arg){
console.log(arg); //[1,2,3,4,5]
let sum = 0;
arg.forEach(function(item){
sum+=item;
});
return sum;
}
add(1,2,3,4,5); //15
//注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
补充arguments
//arguments对象是所有(非箭头)函数中都可用的局部变量。
//你可以使用arguments对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,第一个参数在索引0处。
//是一个对应函数参数的类数组对象。不是一个真正的数组。无法使用数组的方法
function add(){
console.log(arguments);
let arr = Array.prototype.slice.call(arguments,1,3);
let sum = 0;
arr.forEach(function(item){
sum+=item;
});
console.log(sum);
return sum;
}
add(1,2,3,4,5);
call & apply & bind
Function函数运行时存在内部指针对象this,根据函数的调用情况不同,this的指向也不同。
1.作为Dom对象的事件处理函数,使用this时,this指向当前Dom对象
2.作为对象的方法使用时,this指向当前对象
3.作为构造函数使用时,this指向当前构造函数所实例化出来的对象
4.在函数嵌套时,内层函数不会继承外城函数this的指向。
-- 如果想让内层函数,使用外层函数this指向时,可以在外层函数中用一个变量(that)保存this。
--由于作用域链的原因,that对于内层函数是可见的
5.作为全局函数使用this时,this指向window;
当然Function函数的this也是可以改变的。而call,apply,bind等函数,就是为了改变函数this指向而存在的。
每个javaScript函数都是一个对象,而这个对象的构造函数是Function,它有prototype属性指向原型对象,原型对象上挂有call、apply、bind等方法;
let Cat = {
eat:'fish'
}
function Dog(){
this.eat = 'bone';
}
Dog.prototype.say = function(){
console.log(this.eat);
}
let dog = new Dog();
dog.say(); //bone
Dog.prototype.say.call(Cat); //fish
dog.say.call(Cat); //fish
//注意:call 、apply 可以实现类的继承
区别:
相同点:两个方法产生的作用是完全一样的不同点:方法传递的参数不同
call接受的是一个参数列表,而apply()接受一个参数数组。 func.call(this, arg1, arg2);
func.apply(this,[arg1, arg2])
其中this是你想指定的上下文,他可以是任何一个JavaScript对象(JavaScript中一切皆对象),call需要把参数按顺序传递进去,而apply则是把参数放在数组里。
bind()方法也是可以改变函数体内this的指向。
bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入bind(方法的第一个参数作为this,传入bind()方法的第二个以及以后的参数,加上绑定函数运行时本身的参数,按照顺序作为原函数的参数来调用原函数。(就是与call使用方式一样,只是bind不会立即执行)
也就是说,区别是,当你希望改变this指向之后并非立即执行,使用bind()方法。而apply/call则会立即执行函数
let dog_bind = dog.say.bind(Cat);
dog_bind();
箭头函数
基本用法
//ES6 允许使用“箭头”(=>)定义函数。
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
//如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
//如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
//由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
//箭头函数可以与变量解构结合使用。
//箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
使用注意点
(1)函数体内的this
对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4) 箭头函数没有prototype
属性。
箭头函数的this
- 箭头函数不会创建自己的
this,但是可以使用(继承)
- 它只会从自己的作用域链的上一层继承this
- 涉及到函数嵌套,内层的箭头函数的this, 继承于父级
- 如果没有函数嵌套,箭头函数中的this,指向window
//code_one
var name_ = '阿黄';
let obj = {
name_: "ergou",
sayname_1: function () {
console.log(this.name_);
return () => {
console.log('0.0.0.0.0::::::', this.name_);
}
},
sayname_2: () => {
console.log(window);
console.log(this.name_);
return () => {
console.log('1.1.1.1.1.1::::::', this.name_);
}
}
}
//code_two
document.getElementsByTagName('button')[0].onclick = function () {
console.log(this);
}
document.getElementsByTagName('button')[1].onclick = () => {
console.log(this);
}
set & map 数据结构
在ES6之前,存储大量数据时,常用的数据结构就是 数组 ,对象, 或者是数组对象嵌套使用。
在es6 中,新增了两个新的数据结构,– set – map
Set数据结构
基本用法:
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set
本身是一个构造函数,用来生成 Set 数据结构Set
函数可以接受一个数组作为参数(或者具有 iterable 接口的其他数据结构),用来初始化。```js
let my_set = new Set();
my_set.add(‘a’);
my_set.add(‘a’);
[…my_set];//[‘a’]let arr = [1, 2, 3, 2, 2, 5, 8, 4, 5, 87, 1, 2, 5, 5];
let s = new Set(arr);// 去除数组的重复成员
[…new Set(array)]//去除字符串里面的重复字符
[…new Set(‘ababbc’)].join(‘’)
// “abc”## Set实例的属性和方法 Set 结构的实例有以下属性。 - `Set.prototype.constructor`:构造函数,默认就是`Set`函数。 - `Set.prototype.size`:返回`Set`实例的成员总数。 Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。 ### 操作方法 - `Set.prototype.add(value)`:添加某个值,返回 Set 结构本身。 - `Set.prototype.delete(value)`:删除某个值,返回一个布尔值,表示删除是否成功。 - `Set.prototype.has(value)`:返回一个布尔值,表示该值是否为`Set`的成员。 - `Set.prototype.clear()`:清除所有成员,没有返回值。 ### 遍历方法 - `Set.prototype.keys()`:返回键名的遍历器对象 - `Set.prototype.values()`:返回键值的遍历器对象 - `Set.prototype.entries()`:返回键值对的遍历器对象 - `Set.prototype.forEach()`:使用回调函数遍历每个成员 ## Map数据结构 ### 基本用法: ES6之前,对象以键值对来保存数据时,键只能是字符串,这给他带来啦很大的限制 ```js const data = {}; const element = document.getElementById('myDiv'); data[element] = 'metadata'; data['[object HTMLDivElement]'] // "metadata" //由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
//构造函数Map
let myMap = new Map();
m.set(div_,'哈哈哈哈');
m.set(true,'嘿嘿');
console.log(m);
//作为构造函数,Map 也可以接受一个数组作为参数。
// 该数组的成员是一个个表示键值对的数组。
let m = new Map([["key","value"],["num","000001"],["age",120]]);
console.log(m,m.size);
Map 结构的实例有以下属性和操作方法。
size
属性返回 Map 结构的成员总数。- Map.prototype.set(key, value)
set
方法设置键名key
对应的键值为value
,然后返回整个 Map 结构。如果key
已经有值,则键值会被更新,否则就新生成该键。
- Map.prototype.get(key)
-
get
方法读取key
对应的键值,如果找不到key
,返回undefined
。
-
- Map.prototype.has(key)
has
方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
- Map.prototype.delete(key)
delete
方法删除某个键,返回true
。如果删除失败,返回false
。
- Map.prototype.clear()
clear
方法清除所有成员,没有返回值。
遍历方法
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员。
与其他数据结构的互相转换
(1)Map 转为数组
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...
)。
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(2)数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
(3)Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
(4)对象转为 Map
对象转为 Map 可以通过Object.entries()
。 Object.keys() //返回对象的键名 Object.entries() //返回对象的键值对
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
此外,也可以自己实现一个转换函数。
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
(5)Map 转为 JSON
Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
map==>obj==>json
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
(6)JSON 转为 Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
jsonString == > obj ==> map
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
Proxy(代理拦截操作)
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
let obj = {
sex:'man'
}
let myproxy = new Proxy(obj,{
get(){},
set(){},
has(){},
delteProperty(){}
...
});
Proxy 支持的拦截操作一览,一共 13 种。
- **get(target, propKey, receiver)**:拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 - **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 - **has(target, propKey)**:拦截
propKey in proxy
的操作,返回一个布尔值。 - **deleteProperty(target, propKey)**:拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - **ownKeys(target)**:拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - **getOwnPropertyDescriptor(target, propKey)**:拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - **defineProperty(target, propKey, propDesc)**:拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - **preventExtensions(target)**:拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 - **getPrototypeOf(target)**:拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - **isExtensible(target)**:拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - **setPrototypeOf(target, proto)**:拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 - **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效
Object.defineProperty()
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()
来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调
语法
Object.defineProperty(obj, prop, descriptor)
参数
obj
要定义属性的对象。
prop
要定义或修改的属性的名称或 Symbol
。
descriptor
要定义或修改的属性描述符。
返回值
被传递给函数的对象。
描述符(descriptor)
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
这两种描述符都是对象。它们共享以下可选键值
//*数据描述符*
configurable
当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
默认为 false。
enumerable
当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。在枚举对象属性时会被枚举到(for...in 或 Object.keys 方法)
默认为 false。
数据描述符还具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
默认为 undefined。
writable
当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。
默认为 false。
存取描述符还具有以下可选键值:
//*存取描述符*
get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefined。
set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined。
//描述符默认值汇总
拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false。
属性值和函数的键 value、get 和 set 字段的默认值为 undefined。
//描述符可拥有的键值
configurable enumerable value writable get set
数据描述符 可以 可以 可以 可以 不可以 不可以
存取描述符 可以 可以 不可以 不可以 可以 可以
let obj = {};
Object.defineProperty(obj,'name',{
// value:'nihao',
// writable:false,
// enumerable:true,
// configurable:true
get(){
return d;
},
set(v){
d = v;
}
});
Object.defineProperty(obj,'age',{
value:'12',
writable:false,
enumerable:true
});
Promise(异步编程解决方案)
回调地狱
假设场景:
获取学生成绩,并划分等级。
提供一些数据接口,接口数据嵌套,下一个接口数据依赖于上一个接口的返回值
学校 ===>班级 ===> 学生 ===> 成绩 ===> 等级的划分
//学校信息接口:http://xxxx.school.com/info?id='00001';
返回值:[
{className:'三年级2班',classId:'c00001'},
{}...
]
//班级信息接口:http://xxxx.class_info.com/info?classId = classId
$.ajax({
url:'',
data:{},
success:function(res){
$.ajax({
url:'',
data:{},
success:function(res){
$.ajax({
url:'',
data:{},
success:function(res){
}
});
}
});
}
});
回调地狱:回调地狱最主要的就是因为功能逻辑代码嵌套的层次太多,导致可读性降低,维护困难,对代码性能,以及易读性,不友好。
案例:jq_ajax请求的回调嵌套
Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。所谓Promise
,简单说就是一个容器,里面管理着异步的操作,从它可以获取异步操作的消息.本身不是异步的,是里面管理的操作是异步的。
Promise
对象有以下两个特点。
(1)对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
Promise
也有一些缺点。首先,无法取消Promise
,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。第三,当处于pending
状态时,无法得知目前进展到哪一个阶段
基本用法
定义
S6 规定,Promise
对象是一个构造函数,用来生成Promise
实例。
promise 是一个构造函数,需要实例化对象
promise构造函数,接受一个函数作为参数
作为参数的函数也接受两个参数,分别是:
-- resolve
-- reject
-- 它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
--resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”
(即从 pending 变为 resolved),
在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
--reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”
(即从 pending 变为 rejected),
在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
let mypromise = new Promise(function(resolve,reject){
// resolve();
// reject();
if(true){
let obj = {username:'张好人',age:22};
resolve(obj);
}else{
reject('error');
}
});
resolve ,reject 抛出数据以后,使用then() 来接收 (promise实例对象下有个then())
mypromise.then(fn1,fn2);
fn1 ===> function(res){} //用来接收成功状态时的数据
fn2 ===> function(res){} //用来接收失败状态时的数据
demo_one
下面是一个Promise对象的简单例子。
function timeout(ms){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject('done');
},ms);
});
}
timeout(3000).then((r)=>{
console.log(r);
},(r)=>{
console.log(r);
});
上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。
demo_two
// Promise 新建后就会立即执行
let promise = new Promise(function (resolve, reject) {
console.log('Promise'); //1
resolve();
});
promise.then(function () {
console.log('resolved.'); //3
});
console.log('Hi!'); //2
Promise.prototype.then
它的作用是为 Promise 实例添加状态改变时的回调函数。then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
new Promise((resolve,reject)=>{
if(true){
resolve('000');
}else{
reject('1111');
}
}).then((re)=>{
console.log(re);
return new Promise();
},(er)=>{
console.log(er);
}).then((res)=>{
console.log(res);
return '....'
})
Promise.prototype.catch()
Promise.prototype.catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
如果Promise 对象对象状态变为resolved
,则会调用then()
方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected
,就会调用catch()
方法指定的回调函数,处理这个错误。另外,then()
方法指定的回调函数,如果运行中抛出错误,也会被catch()
方法捕获。
new Promise((resolve,reject)=>{
if(false){
resolve('ok');
}else{
reject('err');
}
}).then((res)=>{
console.log(res);
}).catch((er)=>{
console.log(er);
});
promise.all / race
//异步加载图片案例
// Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
// let proall = Promise.all([p1,p2,....]);
1)只有p1、p2、p3的状态都变成成功,proall的状态才会变成成功,此时p1、p2、p3的返回值组成一个数组,传递给proall的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,proall的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给proall的回调函数。
// Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例
// Promise.race 只有有一个promise实例的状态确定了,那真个race的状态就确定了
// 多个实例,谁先加载出来,就执行谁。
综合案例
要求:
封装原生ajax请求,调用接口数据,具体实现本章节开头时的回调地狱的效果。
基于Promise封装ajax请求,调用接口数据,对比体现Promise管理异步状态的优势,功能的强大,一定要掌握promise的用法
async & await
基本用法
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?
- async 关键字,用来修饰函数,把这种函数称之为asyn函数
- async 是es8中,引入的,一个用于异步编程的一种解决方案;
- async函数返回一个 Promise 对象,可以使用then方法添加回调函数。
async
函数内部return
语句返回的值,会成为then
方法回调函数的参数
function fn(){}
console.log(fn()); //undefined
async function fn(){}
console.log(fn()); //Promise PromiseState:'fulfilled' PromiseResult:undefined
async function fn(){
return 0;
}
console.log(fn()); //Promise PromiseState:'fulfilled' PromiseResult:0
//async函数返回一个 Promise 对象,可以使用then方法添加回调函数。
fn().then(res=>console.log(res)); //0
await
- await命令与async配合使用,正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function f2(){
let res = await new Promise((resolve,reject)=>{
if(true){
resolve({msg:'嘿嘿嘿'});
}else{
reject({msg:'error~'});
}
});
console.log(res);
}
f2();
async function f3(){
let res = await 123;
console.log(res);
return res;
}
f3();
console.log(f3());
- 当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
- await 不是同步的,是异步的,遇到await以后,await后的代码会进入任务队列,当所有的主线程上的同步任务执行完以后,才会执行await;
async function f4(){
console.log(111111);
let r = await 22222;
console.log(r);
console.log(33333);
}
f4();
console.log(444444);
- await 命令必须写在async函数中,否则语法错误
async 函数有多种使用形式。
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = {
fn:function(){},
async demo(){},
async foo() {}
};
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};
案例对比
//结合上一章节回调地狱,promise,对比async处理异步操作
1.原生ajax回调地狱
request_ajax('GET', 'http://localhost:4000?name=三体', function (res) {
console.log(res);
let {data: [{author}]} = res;
request_ajax('GET', `http://localhost:4000?author=${author}`,function (re) {
console.log(re);
});
});
2.promise形式
request_ajax_promise('get', 'http://localhost:4000?name=三体')
.then((res) => {
console.log(res);
let { data: [{author}]} = res;
return request_ajax_promise('get', `http://localhost:4000?author=${author}`);
})
.then((res) => {
console.log(res);
return;
});
3.async形式
async function getdata(){
let res = await request_ajax_promise('get', 'http://localhost:4000?name=三体');
console.log(res);
let { data: [{name}]} = res;
return await request_ajax_promise('get', `http://localhost:4000?name=${name}`);
}
getdata().then(res=>console.log(res));
Fetch API
- fetch 基于promise
- 默认是get 方式
- 提供了一个全局 fetch() 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。 这种功能以前是使用 XMLHttpRequest 实现的。Fetch 提供了一个更理想的替代方案
fetch('http://localhost:4000?name=三体').then(res=>{
// 返回的是服务端的响应。不是真正的数据;
// 但是,我们要获取数据,就要 使用一个方法res.json();
// console.log(res);
return res.json();
}).then(res=>{
console.log(res);
let { data: [{author}]} = res;
return fetch(`http://localhost:4000?author=${author}`);
}).then(res=>{
return res.json();
}).then(res=>{
console.log(res);
});
//fetch 与 async 结合使用demo
async function fn() {
let res = await fetch('http://localhost:4000?name=三体');
res = await res.json();
let {data: [{author}]} = res;
let r = await fetch(`http://localhost:4000?author=${author}`);
return await r.json();
}
fn().then(res => console.log(res));
class & module
Class
类的由来
ES6之前,JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。
function Stu(n,a){
this.name = n;
this.age = a;
}
Stu.prototype.say = function(){
console.log(this.name);
}
let stu1 = new Stu('lisi',12);
stu1.say();
这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
//java面向对象封装
public class Demo02Method {
public static void main(String[] args) {
int[] array = {5,15,25,35,111};
int max = getMax(array);
System.out.println(max);
}
public static int getMax(int[] array) {
int max = array[0];
for (int i = 0; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
return max;
}
}
ES6类的定义
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
ES6中,使用class 来定义类,用来生成实例对象。ES6 的类,完全可以看作构造函数的另一种写法。
class 类名{
//默认会存在一个构造器constructor
//constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。
//一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
//class 中的constructor方法,就是构造方法
}
class Point{
constructor(n,a){
this.name = n;
this.age = a;
}
}
//在class中定义的方法,都是挂在在原型对像上的,所有的实例都可以共享。
//定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。
//另外,方法之间不需要逗号分隔,加了会报错
class Point{
constructor(n,a){
this.name = n;
this.age = a;
}
say(){
console.log(this.name);
}
}
使用的时候,也是直接对类使用new
命令,跟构造函数的用法完全一致。如果忘记加上new
,像函数那样调用Class
,将会报错。
let stu1 = new Stu('lisi',34);
console.log(stu1); // Stu {name: "liis", age: 34}
let stu1 = Stu('lisi',34); //err
类的所有方法都定义在类的prototype
属性上面。
class Stu{
say(){},
hello(){}
}
//等同于
Stu.prototype.say = function(){}
Stu.prototype.hello = function(){}
//在类的实例上面调用方法,其实就是调用原型上的方法。
console.log(stu1.say === Stu.prototype.say);
取值函数(getter)和存值函数(setter)
在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
静态方法
所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
console.log(this);// 静态方法中的this,并不指向实例对象;// 此处,this指向class 本身
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
静态属性
静态属性指的是 Class 本身的属性,即Class.propName
,而不是定义在实例对象(this
)上的属性。
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
上面的写法为Foo
类定义了一个静态属性prop
。
目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static
关键字。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
Class 的继承
//回顾:构造函数继承
function Ani(f){
this.foot = f;
this.type = '猫科动物';
}
function tiger(){
Ani.call(this,4);
this.name = 'cookie';
this.age = 2;
}
let t1 = new tiger();
console.log(t1);
Class 可以通过结合extends关键字和 super()方法实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
//父类
class Ani{
constructor(f){
this.foot = f;
this.type = '猫科动物';
}
}
//子类
class tiger extends Ani{
constructor(){
// 子类必须在constructor方法中调用super方法,否则新建实例时会报错。
// 这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,
// 得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。
// 如果不调用super方法,子类就得不到this对象。
super(40);
this.name = 'cookie';
this.age = 2;
}
}
let t1 = new tiger();
console.log(t1);
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;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
export
// 文件 one.js 通过export命令指定输出代码
// 1.
export let a = 10;
export let obj = {
name:"方腊"
}
export function fn(){ alert('export function fn'); }
//ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。
//2.
var a = 10;
let userinfo = {
name:'jq'
}
class Ani{
constructor(){
this.type = '犬';
}
}
export {a,userinfo,Ani};
//上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
//通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
export {a,userinfo as u,Ani};
//注意:export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
// 正确写法一
export var m = 1;
// 正确写法二
var m = 1;
export {m};
// 正确写法三
var n = 1;
export {n as m};
import 命令
文件 two.js
//使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
import {a,obj,fn} from 'one.js';
//从模块one.js中,输入变量, import必须接受一个大括号,里面指定从其他模块导入的变量名,大括号里的变量名,必须与被导入的模块,对外暴露的接口名称相同。
//如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { a, obj as person,fn} from 'one.js'
//import命令输入的变量都是只读的,因为它的本质是输入接口。不允许在加载模块的脚本里面,改写接口。但是,如果a是一个对象,改写a的属性是允许的。
export default 命令
使用import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出。
// ./export-default
export default function () {
console.log('foo');
}
//默认输出是一个函数。
//其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
import customName from './export-default';
customName(); // 'foo'
export default {
num:10,
fn(){}
}
import demo from './export-default';
demo.num //10
Module 的加载实现
1.浏览器加载
浏览器加载 ES6 模块,也使用<script>
标签,但是要加入type="module"
属性。
<script type="module" src="./foo.js"></script> 由于type属性设为module,所以浏览器知道这是一个 ES6 模块。
2.Node.js 的模块加载方法
JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用require()
和module.exports
,ES6 模块使用import
和export
。
它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
Node.js 要求 ES6 模块采用.mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用.mjs
后缀名。Node.js 遇到.mjs
文件,就认为它是 ES6 模块
- 本文链接:https://wangyou.ink/2021/11/30/ES6/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。