let 与 const 命令 以及箭头函数初步学习

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>
WX20200602-113748@2x.png

为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。

  1. 允许在块级作用域内声明函数。
  2. 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  3. 同时,函数声明还会提升到所在的块级作用域的头部。

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 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法: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调用时thiscall方法改变到了对象{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);

上面例子二,①②③处的输出结果如下:

img

很明显,普通函数outer内部的箭头函数fun中的arguments对象,其实是沿作用域链向上访问的外层outer函数的arguments对象。

可以在箭头函数中使用rest参数代替arguments对象,来访问箭头函数的参数列表!!

七、箭头函数没有原型prototype

let sayHi = () => {
    console.log('Hello World !')
};
console.log(sayHi.prototype); // undefined

八、箭头函数不能用作Generator函数,不能使用yeild关键字

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343