ES6学习:哔站小马哥ES6从入门到精通系列(全23讲),开发必备,推荐必看_哔哩哔哩_bilibili+阮一峰文档ES6 入门教程
一、ES6简介
ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等。
二、let和const
1、let不存在变量提升
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined
。但是let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
2、let是一个块作用域(暂时性死区)
只要块级作用域内存在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)。
3、let不能重复声明
let
不允许在相同作用域内,重复声明同一个变量
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
4、const
const也不能重复声明,也是块级作用域,也不存在变量提升,但是和let不同的是,const
一旦声明,常量的值就不能改变。还有就是for循环不能用const。
比如我这么写,var和let都不会报错,但是const会报错:
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
对于const
来说,只声明不赋值,就会报错:(const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值)
const foo;
// SyntaxError: Missing initializer in const declaration
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
上面代码中,常量foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
作用1:for循环中用let比较合适
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000)//5 5 5 5 5
}
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000)//0 1 2 3 4
}
上面的这个for
循环,如果用var i = 0
,那么最后结果是 5 5 5 5 5
,这是因为用var
是在全局只声明一个变量i
,那么每一次循环,全局的i
都会改变,里面的console.log(i)
指向的都是同一个i
,所以最后输出的是55555。
但是
但是如果使用let
声明就不一样了,i
只在当前作用域生效,每一次循环都会重新声明变量。相当于以下代码::
for(let i = 0; i < 5; i++){
let i = 当前i的值;
setTimeout(()=>console.log(i),1000)//0 1 2 3 4
}
也正是因为如此,循环中不能使用const,因为const定义后的变量不可修改,不能在上一轮循环的基础上进行计算(如++)再重新赋值给本轮声明。
作用2:for循环分为父作用域和子作用域
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let 重复声明同一个变量)。
作用3:let和const不会污染全局变量
var RegExp = 100;
console.log(RegExp);//100
console.log(window.RegExp);//100
那么最后的结果是两个100,window对象直接被改了,但是如果用let
let RegExp = 100;
//const RegExp = 100;
console.log(RegExp);//100
console.log(window.RegExp);//function RegExp()
第一个是100,第二个结果还是window对象的RegExp函数。
综上所述:默认情况下用const,如果变量后面需要被修改再用let。
三、模板字符串
比如有一个div我想要js动态的往里面加这些东西,平常的写法是:
<div></div>
<script>
const div = document.querySelector('div');
let id='good';
let name='写代码让我快乐';
div.innerHTML = "<ul><li><p id="+id+">"+name+"</p></li></ul>";
</script>
但是有了模板字符串之后:
使用tab上面那个小点点包起来,里面的变量使用${变量名}来替换,非常方便。
const div = document.querySelector('div');
let id='good';
let name='写代码让我快乐';
div.innerHTML= `<ul>
<li>
<p id=${id}>${name}</p>
</li>
</ul>`
// div.innerHTML = "<ul><li><p id="+id+">"+name+"</p></li></ul>";
四、函数默认值、剩余参数、扩展运算符
1.函数默认值
如果我们想写一个求和函数,在es5中需要这样写:
function add(a, b) {
a = a || 10;
b = b || 20;
return a + b;
}
console.log(add());
但是在es6中我们可以这样写:非常简单
function addd(a = 10, b = 20) {
return a + b;
}
console.log(addd());
但是像下面这样写没有给b赋值会报错:
function addd(a = 10, b) {
return a + b;
}
console.log(addd(20));
赋值也可以是函数比如:
function add2(a = 10, b = getVal(5)) {
return a + b;
}
function getVal(val) {
return val + 5;
}
console.log(add2()); //20
2.剩余参数
把一个对象儿中的属性名和属性值拿出来传给另一个对象。按照ES5的写法,如果不确定用户传几个参数,我们肯定是使用arguments伪数组来接收。
function pick(obj) {
let result = {};
for (let i = 1; i < arguments.length; i++) {
result[arguments[i]] = obj[arguments[i]];
}
return result;
}
let book = {
title: '前端之王',
name: 'c',
age: 18
}
let bookData = pick(book, 'title', 'name', 'age');
console.log(bookData);
但是在ES6中,我们可以获取一个真正的数组。使用(…+名字)这种语法来做形参,返回的是一个数组,但是这个一定要写在形参的最后一个。比如下面这个例子,obj对应book,args就是后面那三个属性名构成的数组
let book = {
title: '前端之王',
name: 'c',
age: 18
}
function pick(obj, ...args) {
console.log(args); //['title', 'name', 'age']
let result = {}; // 定义一个空对象
for (let i = 0; i < args.length; i++) {
//把对象中的属性值拿过来,给空对象(如果该对象没有这个属性,那么就添加一个)
result[args[i]] = obj[args[i]];
}
return result;
}
let bookData = pick(book, 'title', 'name', 'age');
console.log(bookData);
有点迷糊在了解了解;
3.扩展运算符
扩展运算符:将一个数组(或对象)分割,并将数组的各个项作为分离的参数传给函数。其实就是把数组或对象拆开
1、比如我要获取数组的最大值,以前es5我们会使用apply
const arr = [123, 545, 34, 234, 5];
console.log(Math.max.apply(null, arr));
但是现在我们es6可以这样写 es6:
console.log(Math.max(...arr));
2、其实也可以吧对象里面的东西拆开:
let obj1 = {x:100, y:200};
let obj2 = {
a:1,
...obj1,
b:2
}
console.log(obj2); //{a: 1, x: 100, y: 200, b: 2}
四、箭头函数
1.箭头函数的语法
在es6中,ES6 允许使用“箭头”(=>
)定义函数,如果只有一个返回值,我们可以把return省略
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
2.对象中的函数和箭头函数
比如一个Person对象,里面有eat方法:
let person = {
name: "jack",
// 以前:
eat: function (food) {
console.log(this.name + "在吃" + food);
},
// 箭头函数版:
eat2: food => console.log(person.name + "在吃" + food),// 这里拿不到this
// 简写版:
eat3(food){
console.log(this.name + "在吃" + food);
}
}
3.箭头函数的this指向
简单来说,想知道箭头函数中的this指向谁,就看这个箭头函数外边有没有包裹函数,如果它外面有函数,那么this指向的就是外层包裹函数的this,如果没有包裹函数,this指向的就是window
例如:
(1)例1
定义一个函数,定义一个对象,使用bind让函数中的this指向这个对象
这样的话,fn里的this指向的就是obj,返回值返回一个匿名箭头函数,箭头函数中的this指向的和包裹它的fn中的this指向相同,也是obj。
这里还要注意bind的特性,bind不会更改原函数,而是复制一份原函数生成一个新函数,新函数中this的指向改变,所以如果bind更改绑定后,调用fn的话,还是输出window
let obj = { name: 'c' }; //定义一个对象,让fn的this指向这个对象
function fn() {
console.log(this);
return () => {
console.log(this);
}
}
//更改fn函数的this指向
let newfn = fn.bind(obj); //bind不会更改原函数,复制一份新的给newfn
fn(); //window
newfn(); //obj
(2)
如果我定义一个对象,里面写个方法,方法里面的this指向的是函数的调用者,也就是Person这个对象,所以最后输出的结果是18
let Person = {
name: 'c',
age: 18,
getAge: function () {
console.log(this.age);
}
}
Person.getAge(); //18
但是我如果把这个函数写成箭头函数,那么this就往外边查找,指向window
let Person = {
name: 'zzy',
age: 18,
getAge: () => {
console.log(this.age);
}
}
Person.getAge(); //undefined
这里还有个注意点:对象有大括号,但不产生作用域
(3)
比如我定义一个Dj对象,里面有个事件监听,点击页面调用另一个方法
那么这么写会报错,因为this指向的是document,document里面根本没有dance方法
let Dj = {
id: 007,
drop: function () {
document.addEventListener('click', function () {
console.log(this); //document
this.dance();
})
},
dance: function () {
console.log('drop the beat');
}
}
Dj.drop(); //报错
但是如果我把监听事件里的函数写成箭头函数,this的绑定就消失了,此时this指向的是外层包裹函数的this,也就是drop函数里的this,也就是Dj对象,这样的话就可以找到dance方法了
let Dj = {
id: 007,
drop: function () {
document.addEventListener('click', () => {
console.log(this); //Dj
this.dance();
})
},
dance: function () {
console.log('drop the beat');
}
}
Dj.drop(); //drop the beat
那如果我把drop也写成箭头函数呢?因为箭头函数没有this绑定,此时就会往上找,而对象的大括号没有作用域,所以this就会找到window,懂了吗
let Dj = {
id: 007,
drop: () => {
document.addEventListener('click', () => {
console.log(this); //window
this.dance();
})
},
dance: function () {
console.log('drop the beat');
}
}
Dj.drop(); //报错
4.箭头函数的注意事项
1.使用箭头函数,函数内部没有arguments
2.箭头函数不能用来构造函数,因为function函数是一个对象,但是箭头函数不是一个对象,实际上箭头函数就是个语法糖。
(1)箭头函数没有自己的this
对象(详见上文)。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。