ES6 let 与 const 命令 以及箭头函数初步学习
let 与 const
let
let 和const用于定义变量。let类似于之前的var。const是定义常量的。
ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
<script>
{
var a = 12;
let b =10;
}
console.log(a)
console.log(b)
</script>
let使用在for循环中
//let使用for循环参数里只能在循环内部使用
for(let i=0;i<=5;i++){
console.log(i)
}
console.log(i) //not defined
let在for循环添加事件中最典型的使用方式
var btns = document.getElementsByTagName('button');
for(let i=0;i<btns.length;i++){
btns[i].onclick = function(){
console.log(i)
}
}
let 在for循环作为循环参数的时候,其实是俩个作用域,大括号里面的是子作用域,如果里面再次定义与循环参数相同的变量,则不会产生相互影响。
//for其实是俩个作用域,大括号里面是一个子作用域
for(let i=0;i<=3;i++){
let i = 'a';
console.log(i);
}
let不存在变量提升,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
console.log(a) //not defined
let a = 12;
暂时性死区
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区“,let不允许重复定义一个变量。
//大括号括起来的称之为块作用域,有循环 switch结构 if结构
for(let i =0;i<3;i++){
console.log(i)
// let i ='a';
let i = '123';
}
块级作用于
-
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
if(true){
function say(){
console.log(1)
}
}
say(); //1
- ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
if(true){
function say(){
console.log(1)
}
}
say();
</script>
为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
- 允许在块级作用域内声明函数。
- 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
const
-
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
console.log(PI);//TypeError: invalid assignment to const `PI'
上面代码表明改变常量的值会报错。
-
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;
console.log(foo.prop); // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
// "foo" is read-only
上面代码中,常量foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
const a = [];
a.push('Hello'); // 可执行
console.log(a); // Array ["Hello"]
a.length = 0; // 可执行
console.log(a); // Array []
a = ['Dave']; // 报错 TypeError: invalid assignment to const `a'
上面代码中,常量a
是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a
,就会报错。
如果真的想将对象冻结,应该使用Object.freeze
方法。
const foo = Object.freeze({});
console.log(foo);
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
console.log(foo); // Object { }
上面代码中,常量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 种声明变量的方法。
ES6箭头函数、箭头函数与普通函数的区别
一、基本语法
ES6中允许使用箭头=>
来定义箭头函数,具体语法,我们来看一个简单的例子:
// 箭头函数
let fun = (name) => {
// 函数体
return `Hello ${name} !`;
};
// 等同于
let fun = function (name) {
// 函数体
return `Hello ${name} !`;
};
可以看出,定义箭头函在数语法上要比普通函数简洁得多。箭头函数省去了function
关键字,采用箭头=>
来定义函数。函数的参数放在=>
前面的括号中,函数体跟在=>
后的花括号中。
关于箭头函数的参数:
① 如果箭头函数没有参数,直接写一个空括号即可。
② 如果箭头函数的参数只有一个,也可以省去包裹参数的括号。
③ 如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。
// 没有参数
let fun1 = () => {
console.log(111);
};
// 只有一个参数,可以省去参数括号
let fun2 = name => {
console.log(`Hello ${name} !`)
};
// 有多个参数
let fun3 = (val1, val2, val3) => {
return [val1, val2, val3];
};
关于箭头函数的函数体:
①如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }。
let f = val => val;
// 等同于
let f = function (val) { return val };
let sum = (num1, num2) => num1 + num2;
// 等同于
let sum = function(num1, num2) {
return num1 + num2;
};
②如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:
// 用小括号包裹要返回的对象,不报错
let getTempItem = id => ({ id: id, name: "Temp" });
// 但绝不能这样写,会报错。
// 因为对象的大括号会被解释为函数体的大括号
let getTempItem = id => { id: id, name: "Temp" };
③如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void
关键字
let fn = () => void doesNotReturn();
箭头函数最常见的用处就是简化回调函数。
// 例子一
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
// 例子二
// 正常函数写法
var result = [2, 5, 1, 4, 3].sort(function (a, b) {
return a - b;
});
// 箭头函数写法
var result = [2, 5, 1, 4, 3].sort((a, b) => a - b);
二、箭头函数与普通函数的区别
1、语法更加简洁、清晰
从上面的基本语法示例中可以看出,箭头函数的定义要比普通函数定义简洁、清晰得多,很快捷。
2、箭头函数不会创建自己的this(重要!!深入理解!!)
我们先来看看MDN上对箭头函数this
的解释。
箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。
箭头函数没有自己的this
,它会捕获自己在定义时(注意,是定义时,不是调用时)所处的外层执行环境的this
,并继承这个this
值。所以,箭头函数中this
的指向在它被定义的时候就已经确定了,之后永远不会改变。
来看个例子:
var id = 'Global';
function fun1() {
// setTimeout中使用普通函数
setTimeout(function(){
console.log(this.id);
}, 2000);
}
function fun2() {
// setTimeout中使用箭头函数
setTimeout(() => {
console.log(this.id);
}, 2000)
}
fun1.call({id: 'Obj'}); // 'Global'
fun2.call({id: 'Obj'}); // 'Obj'
上面这个例子,函数fun1
中的setTimeout
中使用普通函数,2秒后函数执行时,这时函数其实是在全局作用域执行的,所以this
指向Window
对象,this.id
就指向全局变量id
,所以输出'Global'
。
但是函数fun2
中的setTimeout
中使用的是箭头函数,这个箭头函数的this
在定义时就确定了,它继承了它外层fun2
的执行环境中的this
,而fun2
调用时this
被call
方法改变到了对象{id: 'Obj'}
中,所以输出'Obj'
。
再来看另一个例子:
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
上面这个例子,对象obj
的方法a
使用普通函数定义的,普通函数作为对象的方法调用时,this
指向它所属的对象。所以,this.id
就是obj.id
,所以输出'OBJ'
。
但是方法b
是使用箭头函数定义的,箭头函数中的this
实际是继承的它定义时所处的全局执行环境中的this
,所以指向Window
对象,所以输出'GLOBAL'
。(这里要注意,定义对象的大括号{}
是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中!!)
三、箭头函数继承而来的this指向永远不变(重要!!深入理解!!)
上面的例子,就完全可以说明箭头函数继承而来的this
指向永远不变。对象obj
的方法b
是使用箭头函数定义的,这个函数中的this
就永远指向它定义时所处的全局执行环境中的this
,即便这个函数是作为对象obj
的方法调用,this
依旧指向Window
对象。
四、.call()/.apply()/.bind()无法改变箭头函数中this的指向
.call()
/.apply()
/.bind()
方法可以用来动态修改函数执行时this
的指向,但由于箭头函数的this
定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this
的指向,虽然这么做代码不会报错。
var id = 'Global';
// 箭头函数定义在全局作用域
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
// this的指向不会改变,永远指向Window对象
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
五、箭头函数不能作为构造函数使用
我们先了解一下构造函数的new都做了些什么?简单来说,分为四步:
① JS内部首先会先生成一个对象;
② 再把函数中的this指向该对象;
③ 然后执行构造函数中的语句;
④ 最终返回该对象实例。
但是!!因为箭头函数没有自己的this
,它的this
其实是继承了外层执行环境中的this
,且this
指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new
调用时会报错!
let Fun = (name, age) => {
this.name = name;
this.age = age;
};
// 报错
let p = new Fun('cao', 24);
六、箭头函数没有自己的arguments
箭头函数没有自己的arguments
对象。在箭头函数中访问arguments
实际上获得的是外层局部(函数)执行环境中的值。
// 例子一
let fun = (val) => {
console.log(val); // 111
// 下面一行会报错
// Uncaught ReferenceError: arguments is not defined
// 因为外层全局环境没有arguments对象
console.log(arguments);
};
fun(111);
// 例子二
function outer(val1, val2) {
let argOut = arguments;
console.log(argOut); // ①
let fun = () => {
let argIn = arguments;
console.log(argIn); // ②
console.log(argOut === argIn); // ③
};
fun();
}
outer(111, 222);
上面例子二,①②③处的输出结果如下:
很明显,普通函数outer
内部的箭头函数fun
中的arguments
对象,其实是沿作用域链向上访问的外层outer
函数的arguments
对象。
可以在箭头函数中使用rest参数代替arguments对象,来访问箭头函数的参数列表!!
七、箭头函数没有原型prototype
let sayHi = () => {
console.log('Hello World !')
};
console.log(sayHi.prototype); // undefined