前言

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

上面代码在代码块之中,分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,结果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

上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量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

上面代码中,变量ilet声明的,当前的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;

上面代码中,变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用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 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 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 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法: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

上面代码中,全局变量avar命令声明,所以它是顶层对象的属性;全局变量blet命令声明,所以它不是顶层对象的属性,返回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,内容安全策略),那么evalnew Function这些方法都可能无法使用。

综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。

ES2020 在语言标准的层面,引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this

demo.js

浏览器环境:
console.log(globalThis); //window
node环境
console.log(globalThis); //global

变量的解构赋值

概述

解构赋值是对赋值运算符的扩展。他是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。在代码书写上简洁且易读,语义更加清晰明了;也方便了复杂对象中数据字段获取。

  1. 数组的解构赋值
  2. 对象的解构赋值
  3. 字符串的解构赋值
  4. 数值和布尔值的解构赋值
  5. 函数参数的解构赋值
  6. 圆括号问题
  7. 用途

数组的解构赋值

基本用法

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 // true

    let [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

上面代码对数组进行对象解构。数组arr0键对应的值是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都能取到值。

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

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];

上面代码交换变量xy的值,这样的写法不仅简洁,而且易读,语义非常清晰。

(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 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

image-20201203225226112

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.fooproxy['foo']
  • **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如proxy.foo = vproxy['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并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefinedset
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined//描述符默认值汇总
拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false。
属性值和函数的键 value、getset 字段的默认值为 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)

在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

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 模块使用importexport

它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块