1、let 和 const
ES5 只有全局作用域和函数作用域,没有块级作用域,ES6中新增了let命令和const命令用于声明变量,且类似于C+、Java等语言是块级作用域。const声明一个只读的常量,必须在定义的时候初始化。let和const声明的变量不存在变量提升,但存在暂时性死区。而且相比于var 语法更加严格,不允许在相同作用域内重复声明同一个变量。let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
//不存在变量提升
console.log(m); //错误:ReferenceError: m is not defined
let m=6;
//块级作用域
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
//变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6
//暂时性死区
var tmp = 123;
if (true) {
tmp = 'abc'; // 错误:ReferenceError: tmp is not defined
//只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,凡是在声明之前就使用这些变量,就会报错
let tmp;
}
//不允许在相同作用域内重复声明同一个变量
function func() {
let a = 1;
let a = 2; //错误:SyntaxError: Identifier 'a' has already been declared
}
//const声明一个只读的常量
const mm=12;
mm=13; //错误:TypeError: Assignment to constant variable.
const arr=[1,2,3];
arr.push(4);
//const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
console.log(arr); //[ 1, 2, 3, 4 ]
arr=[]; //错误:TypeError: Assignment to constant variable.
//变量指向的内存地址改变会报错
2、解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。只要等号两边的模式相同,左边的变量就会被赋予对应的值,可以避免对变量一条一条地赋值。
常用的有数组形式的解构赋值和对象的解构赋值。解构赋值的时候也可以指定默认值,只有当一个成员严格等于undefined
,默认值才会生效。
注意:null
不严格等于undefined
// 以前我们给变量赋值,只能直接指定值
var a = 1;
var b = 2;
var c = 3;
console.log(a,b,c); // 1 2 3
// 现在用解构赋值的写法就变得简单了,只要模式匹配上了就行了,如下
// 注意数组是有顺序的
var [a,b,c] = [11,22,33];
console.log(a,b,c); // 11 22 33
var [b,a,c] = [11,22,33];
console.log(a,b,c); // 22 11 33
// 当然解构赋值还有嵌套比较复杂的写法,如下
let [foo,[[bar],[baz]]] = [111,[[222],[333]]];
console.log(foo,bar,baz); // 111 222 333
let [head,...foot] = [1,2,3,4];
console.log(head,foot); // 1 [2,3,4]
// 如果解构不成功,变量的值就等于undefined,如下
var [bar3,foo3] = [1000];
console.log(bar3,foo3); // 1000 undefined
// 另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功
let [x,y] = [10000,20000,30000];
console.log(x,y); // 10000 20000
// 默认值可以引用解构赋值的其他变量,但该变量必须已经声明
let [a=1,b=a] = [2,3];
console.log(a,b); // 2 3
// 对象的解构也可以指定默认值
var {x,y=5} = {x:1};
console.log(x,y); // 1 5
//对象的解构赋值解构不仅可以用于数组,还可以用于对象(json)
//对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;
//而对象的属性没有次序,变量必须与属性同名,才能取到正确的值
var {a,b} = {a:'apple',b:'banana'};
console.log(a,b); // apple banana
var {b,a} = {a:'apple',b:'banana'};
console.log(a,b); // apple banana
// 如果变量名与属性名不一致,必须写成下面这样
let obj = {first:'hello',last:'world'};
// first ---> f,那么此时f就是first,而不是undefined了,有点类似别名的概念
let {first:f,last} = obj;
console.log(f,last); // hello world
//1.也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。 真正被赋值的是后者,而不是前者
//2.v是匹配的模式,n才是变量。真正被赋值的是变量n,而不是模式v。
//注意,采用这种写法时,变量的声明和赋值是一体的
// v ---> n,那么此时n就是vue,而不是undefined了
var {v:n} = {v:'vue',r:'react'};
console.log(n); // vue
console.log(v); // Uncaught ReferenceError: v is not defined
console.log(r); // Uncaught ReferenceError: r is not defined
3、模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,将变量名写在${}中可以向
字符串中嵌入变量。${}大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
注意:如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中
//使用+号连接字符串
var a=12;
var b=36;
var c=a+b;
var str=a+'+'+b+"的结果是:"+c;
console.log(str); //输出:12+12的结果是:24
//使用模板字符串
let x=12;
let y=16;
let resultStr=`${x}*${y}的结果是:${x*y}`;
console.log(resultStr); //输出:12*16的结果是:192
//模板字符串的嵌套,
const str1=`1~9的平方数:
${[1,2,3,4,5,6,7,8,9].map((item)=>{return `${item}的平方:${item*item}`}).join(`
`)}
结束`;
console.log(str1);
//输出:
/*1~9的平方数:
1的平方:1
2的平方:4
3的平方:9
4的平方:16
5的平方:25
6的平方:36
7的平方:49
8的平方:64
9的平方:81
结束*/
4、箭头函数
ES6 允许使用“箭头”(=>)定义函数。
箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
注意:在使用箭头函数时,this的指向是一个重点。箭头函数中的this指向是定义时所在的作用域而不是运行时所在的作用域,也就是定义时代码外层块的this指向。这是因为箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。
//箭头函数的写法
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
//箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分
var sum = (num1, num2) => num1 + num2;
//等同于 箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回
var sum = (num1, num2) => {return num1 + num2};
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
//箭头函数中this的指向
var a=10;
var obj={
a:1,
fn1:function(){
this.a++;
},
fn2:()=>{
this.a++;
}
};
obj.fn1();
console.log(a); //10
console.log(obj.a); //2
obj.fn2();
console.log(a); //11
console.log(obj.a); //2
//这里的this指向window
5、对象的扩展
属性的简洁表示法,ES6 允许直接写入变量和函数,作为对象的属性和方法。
Object.js用来判断两个值是否严格相等,ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。
Object.assign是进行值的浅拷贝而不是深拷贝,对象拷贝得到的是这个对象的引用。
//属性的简洁表示法
var name='cc';
var obj = {
name,
method() {
return this.name;
}
};
console.log(obj.method()); //cc
//等同于
var obj1 = {
name:name,
method:function() {
return this.name;
}
};
console.log(obj1.method()); //cc
//Object.is
console.log(+0 === -0); //true
console.log(NaN === NaN); // false
console.log(Object.is(+0, -0)); // false
console.log(Object.is(NaN, NaN));// true
//Object 用于对象的浅拷贝
var src={a:1};
var src1={a:2,b:{mm:'cc'}};
Object.assign(src, src1);
console.log(src); //{ a: 2, b: { mm: 'cc' } }
src1.b.mm='another';
console.log(src); //{ a: 2, b: { mm: 'another' } }
6、Set与Map
ES6 提供了新的数据结构 Set、WeakSet 、Map、WeakMap 。
Set类似于数组,但是成员的值都是唯一的,没有重复的值。
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。
Map 数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。WeakMap
的键名所指向的对象,不计入垃圾回收机制,只接受对象作为键名(null
除外),不接受其他类型的值作为键名。
Set、Map 结构的实例有四个遍历方法,可以用于遍历成员。
keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器forEach()
:使用回调函数遍历每个成员
//Set基本用法
let set1=new Set();
set1.add(1);
set1.add(1);
set1.add(2);
set1.add(3);
for (let i of set1) {
console.log(i); //1,2,3 注意:成员的值唯一
}
//Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身
let set2 = new Set([1, 2, 3, 4, 4,NaN,NaN]);
let arr=[...set2];
console.log(arr); //[ 1, 2, 3, 4, NaN ]
console.log(set2.size); //5
console.log(set2.has(2)); //true
set2.delete(2);
console.log(set2.has(2)); //false
//Map的基本用法
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
console.log(m.get(o)); // "content"
console.log(m.has(o)); // true
console.log(m.delete(o)); // true
console.log(m.has(o)) // false
//Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
for(let [key,value] of map)
{
console.log(key, value);
}
//name 张三
//title Author
7、for-of循环
ES6引入了for-of循环,作为遍历所有数据结构的统一的方法。
for-in循环获取键名,for-of循环直接获取键值,且没有for-in循环的一些缺点。
for-of循环默认调用 Iterator 接口,原生具备 Iterator 接口的数据结构:Array、Map、Set、String、TypedArray、arguments 对象、NodeList 对象。
关于JavaScript中forEach、for-in、for-of循环的比较,可以看这篇博客:https://blog.csdn.net/cc_fys/article/details/78161573
//遍历Array
var arr=[2,56,78,34,7];
for(var i of arr)
{
console.log(i); //2 56 78 34 7
}
//遍历Map
var map = new Map().set('age', 10).set('name', 'cc');
for (var pair of map) {
console.log(pair); //[ 'age', 10 ] [ 'name', 'cc' ]
}
//遍历Set
var set=new Set();
set.add('name').add('age');
for (var j of set) {
console.log(j); //name age
}
//遍历String
for (var n of 'mystr') {
console.log(n); //m y s t r
}
//遍历TypedArray
var int16 = new Int16Array(2);
int16[0] = 42;
for (var t of int16) {
console.log(t); //42 0
}
//遍历arguments
function exa(){
for (var ar of arguments) {
console.log(ar); //go 67 dog
}
}
exa('go',67,'dog');
//遍历NodeList 对象
var paras = document.querySelectorAll("div");
for (let d of paras) {
console.log(d);
}
8、promise对象
Promise被设计用于改善JS中的异步编程,与事件及回调函数相比,在异步操作方面提供了更多的控制权与组合性。可以避免层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
Promise有三种状态:挂起、已完成、已拒绝。
Promise.race
方法和Promise.all方法
是将多个 Promise 实例,包装成一个新的 Promise 实例。Promise.all
方法接受一个数组作为参数,只有传入的所有Promise都完成,返回的Promise才能完成。如果有任何Promise被拒绝,返回的主Promise就立即会被拒绝。Promise.race
方法,只有第一个决议的promise取胜,并且其决议结果成为返回Promise的决议。
//promise基本用法
//Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject
//Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
//then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve(54);
});
setTimeout(function(){
console.log('timeout');
},0);
promise.then(function(value) {
console.log(value);
});
console.log('Hi!');
//输出:
//Promise
//Hi
//value
//timeout
//说明:Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在本轮事件循环的末尾执行,setTimeout(function(){},0)将在下一轮事件循环的开始执行
//Promise.all与Promise.race
let p1=Promise.resolve(42);
let p2=Promise.resolve('hello world');
let p3=Promise.reject('Oops');
Promise.race([p1,p2,p3]).then(function(msg){
console.log(msg); //42
});
Promise.all([p1,p2,p3]).catch(function(err){
console.log(err); //oops
});
Promise.all([p1,p2]).then(function(msgs){
console.log(msgs); //[ 42, 'hello world' ]
});
9、Generator生成器、Iterator迭代器
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of
循环,Iterator 接口主要供for...of
消费。
Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next
方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next
方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next
方法,直到它指向数据结构的结束位置。
执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态
//生成器
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
console.log(hw.next()); //{ value: 'hello', done: false }
console.log(hw.next()); //{ value: 'world', done: false }
console.log(hw.next()); //{ value: 'ending', done: true }
console.log(hw.next()); //{ value: undefined, done: true }
//helloWorldGenerator 函数会返回一个迭代器对象
//使用for...of循环遍历
for(let item of helloWorldGenerator())
{
console.log(item);
}
//输出:
//hello
//world
//迭代器
function idMaker() {
var index = 0;
return {
[Symbol.iterator]:function(){
return this;
},
next: function() {
return {value: index++, done: false};
}
};
}
var it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
//当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。也就寻找idMaker()中的Symbol.iterator属性
for(let item of idMaker())
{
if(item<5)
console.log(item); //0 1 2 3 4
else
break;
}
10、class
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。
类和模块的内部,默认就是严格模式,所以不需要使用use strict
指定运行模式。
Class定义的类不存在变量提升,需要在使用之前定义。
class Foo{
constructor(a,b)
{
this.x=a;
this.y=b;
}
gimmeXY(){
return this.x*this.y;
}
}
var f=new Foo(5,15);
console.log(f.x); //5
console.log(f.y); //15
console.log(f.gimmeXY()); //75
//class中的继承
class Bar extends Foo{
constructor(a,b,c){
super(a,b);
this.z=c;
}
gimmeXYZ(){
return super.gimmeXY()*this.z;
}
}
var b=new Bar(5,15,25);
console.log(b.x); //5
console.log(b.y); //15
console.log(b.z); //25
console.log(b.gimmeXYZ()); //1875
《你不知道的JavaScript》(中卷)