8.模块化

8.1 什么是模块?

模块(modules)就是使用不同的方式加载的js文件.模块代码和脚本(script)有不同的语义.

  1. 模块代码自动运行在严格模式下,并且没有任何办法跳出严格模式
  2. 在模块代码的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,只会在模块顶级作用域的内部存在
  3. 模块的顶级作用域的this值为undefined,
  4. 模块不允许在代码中使用HTML风格注释
  5. 对于需要让模块外部代码访问的内容,必须导出它们
  6. 允许模块从其它模块导入绑定._

模块的主要能力是按需导出和导入代码的能力.,不用把所有的内容放在同一个文件内

基本的导出

可以使用export 关键字把已经写好的功能代码,拆分之后给其它的模块.最直接的方法就是export放置在任意的变量,函数或类声明之前.

export var user = "ggb";
export let age =  11;
export const a = 1;

export function num(){
    return 1;
}

export class users {
    constructor(){
        this.len = 1;
        this.str = '2'
    }
}

function sub(){
    return 2;
}

export default sub
export {
   //按需导出
}

基本的导入

导入的前提是有导出的模块.可以在其它的模块代码中,使用 import 关键字来访问已经被导出的功能.

import有两个部分.一个是需要导入的标识符,一个是来源模块

基本语法

import {sub,a} from 'app.js'

单个的绑定

<script type="module">
    import {sub} from './app.js'
    console.log(sub())
    sub = 1;  //报错
</script>

我们app.js,导出了两个.但是在上面这个案例中.仅仅只导出了一个函数,如果说要给这个sub赋值,是不允许,不能导入的绑定重新赋值.

在导入的文件名前要使"/","../","./"

如果想入多个

import {sub,a,user} from './app.js'
console.log(sub(1,4))
// sub = 1;
console.log(user)

想要所有的导入

import * as obj from './app.js'
console.log(obj.sub(1,4))
// sub = 1;
console.log(obj.user)

无论对同一个模块使用了多少次的导入import 但是这个模块都会被执行一次

import {sub} from './app.js'
import {a} from './app.js'
import {user} from './app.js'

这里导出三次….但是app.js都只会被执行一次.

7.Promise

背景

Promise 主要的功能就是为了更好的解决异步编程问题.工作方式就是用于延迟代码执行并在未来执行当前这个作业.

一个Promise指定一些稍后要执行的代码,这个工作机制就和我jquery AJAX 中回调函数一样.明确标示作业代码是..否执行成功.以成功处理或失败处理为准.把promise串联在一起.

异步编程的背景

JS引擎建立在单线程事件循环的概念上,什么是单线程:单线程意味着同一时刻只能执行一段代码.这和其它语言比C++,JAVA,可以同时执行多段不同代码的多线程语言不一样.

多段代码可以同时访问或修改状态.但是维护很难.

JS引擎因为同一时刻只能执行一段代码.所以不用管另外的可能会运行的代码.js代码在执行中会把代码放置在作业队列中(job qeeue) ,在我们js代码中每一段准备被执行,就会被添加进作业队列.当结束当前代码执行后,就会进入下一个代码作业.这就是所谓的事件循环.是一个内部处理线程,可以监视代码执行并管理作业队列,因为是队伍.所以一定是依次进行.直到最后一个.

7.1 事件模型

当用户点击一个按扭或在键盘上按下一个健时.一个事件(event),比如onclick被触发.这个事件可能会对此交互进行响应,从而把一个新的代码作业添加到我当前的作业的队列尾部.这就是异步编程的最基本的形式.事件事处理程序代码一直到事件发生后才会被执行.比如:

let oBtn = document.getElementById('btn');
oBtn.onclick=function(){
    alert('中国')
}

以上代码,alert(中国) 直到oBtn点击之后才会执行.当被点击之后.赋值给onclick的匿名函数就被添加到了作业队列的尾部,并在队列前所有的任务结束之后再执行

7.2 回调模式

回调函数模式类似于事件模型.因为也会在后面一个时间点执行.不同的地方是回调是以参数的形式传入.

//下课后补全

$.ajax({
    url:'1.txt',
    success:function (res) {
         $.ajax({
             url:'1.txt',
             success:function(){
                 $.ajax({
                         url:'1.txt',
                         success:function(){
                             $.ajax({
                                     url:'1.txt',
                                     success:function(){
                                         $.ajax({
                                                 url:'1.txt',
                                                 success:function(){

                                                 }
                                             }
                                         )
                                     }
                                 }
                             )
                         }
                     }
                 )
             }
         }
    )
    }
})

7.3 Promise 基础

个人认为Promise是ES6,夜空中最亮的星.是为异步操作的结果所准备的占位符,可以用函数返回一个Promise,不需要事件或回调函数.比如:

//readFile函数会在某个时间点完成作业并赋值给promise
let promise = readFile('1.txt')

在这行代码中.我们可以理解为 readFile(‘1.txt’) 并没有马上读取1.txt文件,可能会在稍后发生.这个函数会返回一个promise的对象以表示异步读取操作.可以在未来特定时间再来操作它,对结果进行操 作的时间.取决promise的生命周期是如何进行的;

7.4 Promise 的生命周期

有一个短暂的生命周期,初始为挂起,表示异步操作尚未结束,一个被挂起的promise,会被认为是未决的.在上面的一个例子中,promise会在readFile()返回它的时候就处于一个挂起状态.一旦异步操作结束,promise就会被认为是已决的.并进入两种可能的状态之一:

  1. 已完成:异步操作已成功结束
  2. 已拒绝:异步操作未成功结束,可能是一个错误,也可能是其它原因导致的.

相差词汇解释:

  1. pending:挂起,表示未结束的promise状态,相关专业词汇"挂起态".
  2. fulilled:已完成,表示已经成功结束的promise状态,可以理解为成功完成,相关专业词汇:"完成","被完成","完成态".
  3. rejected:已拒绝,表示已结束但失败的promise状态,相关词汇,"拒绝","被拒绝","拒绝态".
  4. unsettled:未决,未解决,表示promise尚未被完成或拒绝,与挂起是近义词.
  5. seteled:已决,已经解决,表示promise已经被完成或拒绝,注意这个与"已完成"或"已决议"不同,"已决的状态可能是,拒绝态(失败)
  6. resolve:决议,表示把promise推向成功态,,可以理解为决义通过,在promise概念中与完成是近义词.相关词汇"决义态",’已决议’,"被决议".
  7. fulfillment handler: 完成处理函数,表示promise为完成态时会被调用的函数
  8. rejection handler:拒绝处理函数,表示promise为拒绝态时会被调用的的函数.

在promise内部有 一个promiseState属性会被设置为,"pending(挂起)","fulilled(已完成,)","rejected(已拒绝)",用来反应promise的当前状态.注意这属性并没有在promise对象中暴露出来.所以我们无法以编程的方式判断当前promise处理什么状态.但是可以使用 then() 方法 在promise的状态改变时执行一些操作.

then()方法在所有的promise上都存在.接受两个参数,第一个参数是被完成时要调用的函数,与异步操作关联的任何所有数据都会被传入这个函数.第二个参数是被拒绝时要调用的函数.和完成函数相似.拒绝函数也会被入与拒绝相关的数据.

传递给then()方法的两个参数都是可选的.比如:

let promise = new Promise((resolve,reject)=>{
    resolve('success')
    // reject()
});

console.log(promise)
promise
.then((res)=>{
    console.log(res)
},(err)=>{
    console.log(err)
})
.then(()=>{
    console.log('fdsfs')
})
.then(()=>{
    console.log('可以无限调用')
})
.catch(function(){
    console.log('这里是重大的错误')
})
// 这个方法的行为拒绝处理给then(),catch()和then()方用是等效的
promise.catch(function(err){
    console.log(err.message)
})

promise
.then(null,(err)=>{
    console.log(err)
})

就算是完成或是拒绝处理的函数在已经被解决之后添加到队列作业,还是被执行.允许你随时添加新的完成或拒绝处理函数.比如:

let promise = new Promise((resolve,reject)=>{
    resolve('success')
    // reject()
});

// 原始的完成处理
promise.then(function(res){
    console.log(res)
    // 添加另一个
    promise.then(function(res){
        console.log(res)

    })
})

以上代码中then()又为同一个promise添加了一另一个完成处理函数.这个promisen已经完成了.因此新的处理程序.就被加入到了任务队列中.在前面的执行完毕之后…再以同样的方式执行

调用 resolve() 触发了一个异步操作,在每次调用then()或catch()都会创建一个新的作业,会在promise已经决议时被执行.并且把它他们添加给了作业队列再执行:比如:

let promise = new Promise(function(resolve,reject){
    console.log('promise')
    resolve()   
});

promise.then(function(){
    console.log('resolved')
})

console.log('hello')

//结果:
promise
hello
resolved

尽管then()方法出现在hello之前.实际上会被稍后执行.和执行器中的console.log(‘promise’)不同.完成处理函数和拒绝函数总是会在执行操作结束之后添加到作业队列的尾部.

基于Promise构造器行为的动态本质.是创建未决的最好的方式.如果想要一个promise代表一个已知的值.传值给resolve()函数并没有多大的意义.有两种方法可以指定值来创建已决的promise

  1. Promise.resolve()

接受一个参.返回一个完成状态.比如

let promise = Promise.resolve('hello')

promise.then(function(val){
    console.log(val)
})
  1. Promise.reject()

创建一个已拒绝的promise

let promise = Promise.reject('hello')
promise.catch(function(val){
    console.log(val)
})

7.5 AXIOS基于一个promise封装的一个ajax框架

7.Set与Map

7.1 Set

是一个无重复值的有序列表.允许对它包含的数据进行快速的访问.增加了一个追踪离散值的方式.

7.1.1 创建set并添加项目.

let set = new Set();
set.add(1)
set.add('1')
console.log(set.size)

以上使用了add()方法向set中添加了项目.可以通过size属性能查看有多少项.

Set不会使用强制类型的转换来判断当前值是否重复.允许存在不同类型的相同值,但是有一个例外 0

-0 和 +0 被认为是一样的

let set = new Set();
set.add(1)
set.add(-1)
set.add(0)
set.add(-0)
set.add(+0)
set.add('1')
console.log(set.size)  //4

也可以添加多个对象,但是多个对象并不会认为是一同一项

let set = new Set(),
    a = {},
    b = {};

    set.add(a)
    set.add(b)
    console.log(set.size)  //2

如果add()方法对相同的值进行多次调用,会在第一次之后被忽略;

let set = new Set();
set.add(1)
set.add('1')
set.add(1)
set.add(1)
console.log(set.size)  //2

当然也可以用数组来初始化Set,且Set会确保不重复的使用这些值.比如:

let set = new Set([1,2,3,4,3,4,3,1,4,6,3]);
console.log(set.size) // 5

在以上这个例子中,数组中的重复值被去除了.虽然当中的数字很多都出现多次.但是只会存一个.

可以使has()方法来判断一个值是否存于Set中,比如:

console.log(set.has(19))  //false

7.1.2 移除值

想要把Set中的值移除.可以使用 delete() 方法来实现,或着也可以用调用clear()方法来把所有的值从Set中移除

比如:

let set = new Set([1,2,3,4,3,4,3,1,4,6,3]);
console.log(set.size) // 5
console.log(set.has(6))  //false
console.log(set.has(19))  //false
set.delete(6)  //删除
console.log(set.has(6))  //false
set.clear() //清除所有
console.log(set.has(1))  //false
console.log(set.size) // 0

所有的这些方法都都有提供一个机制追踪唯一值,不过,在给Set添加之后,如果想要对每个项执行 一些操作,需要用到forEach()方法.

7.1.3 set.forEach()方法

这个的是属于Set上的forEach()方法,在之前的forEach方法,可以更容易处理数组中的数组.不用建立循环.于是在Set类型也加了相同的方法.工作方式是一样的.

接受三个参数,

  1. Set中下个位置的值
  2. 和第一个参数相同的值
  3. Set本身

set中forEach和数组中的版本有一点点差别.set版本传给回调函数的第一个和第二个是相同的…Set没有所谓的健.具体的使用方法.

let set = new Set([1,2]);

set.forEach(function(value,key,selfSet){
     console.log(value,key)
     console.log(selfSet === set)
})
//结果
1 1
true
2 2
true

以上代码在每一项中进行了迭代,并对传递给forEach(),回调函数的值进行了输出.回调函数每次执特时.key和value总是相同的,同时selfSet和set 也是相同的.

 7.1.4 把Set转换为数组

可以使用剩余参数把set赋值给一个新的数组变量.

let set = new Set([1,2,4,5,3,3,5,6,3])
let arr = [...set];

console.log(arr)  //[1, 2, 4, 5, 3, 6]

以上代码中,set在初始化的时候包含了一个有重复值的数组.在清除重复值之后.又使用了扩展运算符把自己的项放到了一个新的数组中.这一步只是把这数组中的每一个项复制给了新数组.本身依旧存在.

7.2 Map

Map类型是键值对的有序列表.健和值可以是任意的类型.

可以使用map.set()方法传递一个健和一个值,来添加项.

可以用键名来调用map.get()方法来取得对应的值

let map = new Map();
map.set('name','朱鹏富');
console.log(map.get('name'))

以上案例存用name为键,朱鹏富为值.接着使用get()方法取得对应的值.如果不存在.就是undefined;

也可以使用对象来创建键.在这之前是不可能做到的.

let map = new Map();
let key1 = {},key2 = {};
map.set(key1,100);
map.set(key2,200)
console.log(map.get(key1))
console.log(map.get(key2))

以上用了对象作为map的键.并了两个值,因为这些健不会被转换为其它形式,每个对象又是唯一所以就允许我们关联额外数据.不用去修改对象自己本身.

7.2.1 Map的方法

Map和Set共享了几个方法.允许我们使用相似的方法来进行map和set的交互.

  1. has() 判断指定的键是否存在
  2. delete() 删除map中的健以及对应的值
  3. clear() 删除所有的map中的健与值
  4. 同样有一个size属性.用于指定有多少个的键值对.

示例:

let map = new Map();
map.set('name','朱鹏富');
map.set('age','18');

console.log(map.size)  //2

console.log(map.has('age'))  //true
console.log(map.get('age'))  //18

map.delete('name');
console.log(map.has('name'))  //false
console.log(map.get('name'))  //undefiend
console.log(map.size)  //1

map.clear();
console.log(map.has('name'))  //false
console.log(map.get('name'))  //undefiend
console.log(map.has('age'))  //false
console.log(map.get('age'))  //undefiend
console.log(map.size)  //0

size属性包含了map中的键值对的数量.分别演示各方法的结果.

 7.2.2 Map的初始化

和Set一样,可以把数组给Map,比如:

let map = new Map([['name','朱澳'],['age','18']]);

console.log(map.has('name'))   //true
console.log(map.get('name'))   //朱澳
console.log(map.has('age'))    //朱澳
console.log(map.get('age'))    //18
console.log(map.size)   //2

以上代码通过Map构造器进行了初始化.name.age添加到了map的变 量中.由数组的形式构成.允许键是任意类型.使用数组是确保它们在添加到map之前不会被强制转为其它类型的唯一方法

7.2.3 Map.forEach()方法

方法类似于set和数组中的同名方法,接受一个回调函数.回调函数接受三个参数:

  1. map下位置的值
  2. 这个值对应的健
  3. Map本身自己

回调函数的这些参数和数组中的forEach方法的行为基本上契合的.也就是第一个在数是值,第二个参数是健

在数组中是索引值.比如:

let map = new Map([['name','朱棣'],['age','18']]);
map.forEach(function(value,key,self){
    console.log(value,key,self)
})

forEach回调函数传递了具体的每个参数的详细信息.

6.符号 Symbol

新的基本类型:符号 (Symbol),刚刚设计出来的时候的被用于创建对象的私有成员.也是javascript开发者一直期待的一个功能特性.这个私有名称最后就成了ES6 中的符号.

6.1 创建symbol

Symbol没有字面量的形式.可以用全局的symbol函数来创建一个符号值:

let first = Symbol('frist');
let obj = {};

obj[first] = '大明王朝1566';
console.log(obj[first])

以上代码创建了一个symbol类型 frist 变量.并把它作为obj对象的一个属性.每次访问属性都要使用这个symbol符号值.

因为symbol基本类型值.因为不能使 new 操作符,如果使用会报错.但是可以通过 new Object(Symbol()) 来创建一个符号实例,在经验中和 Symbol() 没有任何的区别.

Symbol还可以接受一个参数用于描述符号值.可用开发调试.比如:

let first = Symbol('frist');
let obj = {};

obj[first] = '大明王朝1566';
console.log(obj[first])

console.log(first)  //Symbol(frist)

符号的描述信息存在内部属性[[desription]]中.当Symbol的toString()方法被调用(隐式或显示).这个信息就会被读取.

因为是基本类型,所以可以使用typeof 操作符来判断一个变量是否为Symbol类型

let first = Symbol('frist');
console.log(typeof first)  //symbol

6.2 使用Symbol

以上的例子已经展示了符号的方括号的用法,还能在对象的"需计算字面量属性名"中去使用Symbol.也可以在Object.defineProperty() 或 Object.defineProperties() 调用中使用它,比如:

let first = Symbol('frist');
// 使用一个需计算的字面量属性
let obj = {
    [first]:"朱高炽"
}
// 让这个属性变为只读
Object.defineProperty(obj,first,{
    writable:false
});

let last = Symbol('last');
Object.defineProperties(obj,{
    [last]:{
        value:"朱厚照",
        writable:false
    }
})

console.log(obj[first])
console.log(obj[last])

以上使用了字面量属性方式创建了一个符号类型的属性first,可以使用Object.getOwnPropertyDescriptor()查看属性enumerable:true.显示是可以允许遍历,但无法用for-in遍历,也不会显示在Object.keys()结果中。

6.3 共享

在不同的代码中使用相同的符号值,比如,在一个应用中需要在两个不同的对象类型中使用同一个符号属性。用来表示一个唯一的标识符。为此ES6提供了一个“全局符号注册表”,允许我们在任意时间点上去访问。

如果想要创建一个共享符号,需要使用symbol.for()方法,比如:

let userid = Symbol.for("userid")
let obj = {};
obj[userid] = '4324234243';
console.log(obj[userid]);
console.log(userid); //Symbol(userid)

Symbol.for()方法首先第一步会搜索全局的符号注册表。查询是否存在一个名为“userid”的Symbol的值。如果存在,这个方法会返回已经存在的值,否则就会创建一个新的值。并且使用这个值把它记录到全局符号注册表中。再返回新值。

这就是说之后使用同一个值去调用 Symbol.for()方法。都会返回同一个符号值。比如:

let u = Symbol.for('u');
let obj = {
    [u]:'1566'
}

console.log(obj[u]);
console.log(u)

let u2 = Symbol.for('u');

console.log(u2 === u)  //true
console.log(obj[u2]);
console.log(u2) //Symbol(u)

在以上这个案例中。u和u2包含同一个symbol值。因此可以互换使用,第一次调用Symbol.for()方法。创建了这个符号值,而第二次调用全局符号注册表中把它搜索出来。

另外还有一个方法Symbol.keyFor()方法可以在全局符号注册表中根据符号值找出对应的键值。比如:

let name = Symbol.for('name');
console.log(Symbol.keyFor(name))  //name

let name2 = Symbol.for('name');
console.log(Symbol.keyFor(name2))  //name

let name3 = Symbol('name');
console.log(Symbol.keyFor(name3))  //undefined

在以上这个案例中.name 和 name2 都返回了键值 "name".但是name3.在全局符号注册表中不存在.所以并没有关联的健值.所以只能是undefined.

全局符号注册表,可以简单的理解为全局作用域.一个共享的环境.

5.解构 – 更方便的数据访问

对象和数组的字面量是我们最常用的两种方式,已经是我们这门语言很重要的组成部分.在写代码的时候,基本上任何时候都会在定义对象和数组.定义之后再从这些结构中取出数据.

ES6增加了解构功能.是一个把数组结构分解到更小更精更细的部分的过程 .今天的课程以此展开.

5.1 解构有什么用

在ES6之前,从对象或是数组中获取数据,或是信息.需要写很多相似的代码

比如:

let options = {
    x:true,
    y:false
}

let x = options.x,
    y = options.y;

以上的代码显示了从对象中取出数据,赋值给了同名的本地变量上.如果只有这样两个.其实也还好.但是如果想象一下有100个.我们是不是需要一个个拿出来赋值,并且如果有一个嵌套的数据结构还要遍历寻找信息,在这种情况下.可能我们只是想要拿出其中一个小节的数据.去深挖整个数据结构。

基于这种情况。ES6增加了个解构赋值的功能。

5.2 对象解构

对象解构在赋值语法的左侧用了对象字面量,比如:

let user = {
    name:"赵中",
    age:20
}

let {name,age} = user;

console.log(name)
console.log(age)

以上代码中.user.name的值被赋值给了本地的变量 name ,user.age给了本地变量age.以上这种写法,属于简写的属性初始化器.name,age标识符代表了本地变量.也读取了相应的属性值

当使用解构来配合var,let,const 来声明变量时.必须提供初始化(也就是等号右边的值) 比如:

var {name,age};       //语法错误
let {name,age};       //语法错误
const {name,age}      //语法错误

const 本身的要求就是必须要有初始化.就算是没有使用解构,也是必须要有初始化的值,

let,var则仅仅在使用解构的时候才有此要求;

5.2.1 解构赋值

以上案例用于变量声明,但是,也可以在赋值的时候使用解构.比如,我想在变量声明之后改变它们的值:比如

let user = {
    name:"赵中",
    age:20
};
let name = '颜色';
let age = 22;

// 使用解构来分配不同的值
({name,age} = user)

console.log(name)
console.log(age)

在以上这个案例中,name和age属性在声明时被初始化,而同时有两个同名变量也被声明且初始化为不同的值,接下来有一行使用解构表达式,能过读取user对象来更改这两个 变量的值.

这里需要注意的是必须用圆括号包裹解构赋值语句,这是因为暴露的花括号会被解析为代码块语句,而块语句不允许在赋值操作符左侧出现.圆括号标示的的意思是花括不是块语句,而被解释为表达式.从而允许完成赋值操 作.

解构赋值表达式的值为右侧赋值操作符之后的值,就是说在任何期望有值的位置都可以使用解构赋值表达式.比如:

let user = {
    name:"赵中",
    age:20
};
let name = '颜色';
let age = 22;

function out(value){
   console.log(value===user); //true
}

out({name,age}=user)

console.log(name)
console.log(age)

以上这个函数在被使用一个解构赋值表达式进行了调用,这个表达式的计算结果为user,因为表达式右侧的值.对name,age的赋值正常进行.同时user也被传入out函数

在使用解构赋值表达式的时候右侧的计算结果如果是null或是undefiend,会出现错误.因为任何读取null或undefined的行为都导致运行时错误.(runtime error)

5.2.2 解构默认值

在使用解构赋值语句时,如果我们所指定的本地变量在对象中没有找到同名属性,哪么这个变量的值赋值为undefined

let user = {
    name:"赵中",
    age:20
};

let {name,age,job} = user;

console.log(name)
console.log(age)
console.log(job)

以上代码定义了一个额外变job,并且试图希望对其赋值.但是user中不存在同名属性,因此job理所当然赋值为undefined.

当然可以为他定义一个默认值,在指定属性不存在的时候使用这个默认值。如果想要实现这样的效果,比如:

 let {name,age,job = true} = user;

以上的变量job被指定了一个默认值,这个默认值只有在user对应的属性缺失的情况下,或对应的属性值为undefined的情况下。才会被启用。比如:

let user = {
    name:"赵中",
    age:20,
    job:'前端工程师'   //如果这里有值,并不为undefined 就使用这里的值,否则就用true
};

let {name,age,job = true} = user;

console.log(name)
console.log(age)
console.log(job)

5.2.3 赋值给不同的本地变量

以上的每个案例,我们都使用了对象中的属性名作为本地变量的名称,如果使用相同的名称,这样做是没有任何的问题的,但是如果你不想使用属性中的同名属性作为本地变量?

ES6有一个扩展语法。允许你在给本地变量赋值的时候使用一个不同的名称。比如:

let user = {
    name:"赵中",
    age:20,
    job:'前端工程师'
};

let {
    name:n,
    age:a,
    job:j
} = user

console.log(n)
console.log(a)
console.log(j)

在以上为个案例中,我们使用了解构赋值来声明了三个变量。n,a,j, 并且成功的获取了user.name;user.age;user.job属性的值。name:n 这种语法表示读取user.name的属性。并且把它的值存在n变量之上。

这种语法和传统的对象字面量的语法相反。传统的是左侧为名,右侧为值,但是在当前这个案例中。左侧为值,右侧为名。

注意:依然可以给变量别名添加默认值,依然是在本地变量名称后添加等于默认值。比如:

let user = {
    name:undefined,
    age:20,
    job:'前端工程师'
};

let {
    name:n = '颜色',
    age:a,
    job:j
} = user

console.log(n)   //颜色
console.log(a)
console.log(j)

在以上这个案例中 n 变量有一个默认值 “颜色” 这个变量值。会在对应的对象属性不存或为undefiend时启用。

5.2.4 嵌套的对象解构

类似于对象字面量的语法,可以使到嵌套的对象结构中的数据,比如:

let user = {
    name:"颜苗",
    love:{
        s:{
            sex:"男",
            number:5
        },
        e:{
            sex:"女",
            number:7
        }
    }
}

let {
    love:{s}
} = user

console.log(s)   // {sex: "男", number: 5}

以上案例在解构中使用了花括号,表示应当下行到user对象中的love属性内部对找s属性,这里需要记住。每当有一个冒号(:)在解构中模式中出现,则表示冒号(:)之前的标识符代表需要检查的位置,而右则是赋值的目标。当右侧存在花括时,表示目标被嵌套对象更深的一层当中。

如果需要更进一步,在对的嵌套解构名同样能为本地变量使用不同的名称,比如:

let { love:{s:php}} = user

以上user.love.s的值被赋值给了一个新的变量php。

5.3 数组解构

数组解构和对象解构在语法上面非常的相似,只不过就是把对象字面量换成了数组字面量。

数组解构时,解构作用在数组的内部位置上,而不是作用在对象的具名属性上。比如:

let users = ["赵中","颜苗","周斯佳"];
let a = users[0],b=users[1]  //es5
let [ z,y ] = users;
console.log(z,y)  // 赵中 颜苗 

以上从数组解构中取出了赵中和颜苗,并且赋值给了z和y变量。这些值被选择。取决于他们在数组中的位置。实际的变量名称是任意的。(和位置无关)。任何没有在解构模式明确指定的值都会被忽略。要注意的是,数组本身没有变任何方式方法改变。

let [,,j] = users;
console.log(j)  //周斯佳

以上案例中加的逗号是为了数组前面的项提供的占位符。使用这种方法可以取出数组任意位的值。不用提供其它的变量名。

需要注意的是。在使用var,let,const进行数组解构时,必须提供初始化值。

我们还可以在赋值表达式中使用数组解构。但是和对象解构不同,不用把表达式包含在圆括号内,比如:

let users = ["赵中","颜苗","周斯佳"],
    z = "张也",
    y = "叶彬";

[z,y] = users;

console.log(z,y)  // 赵中 颜苗

在ES5如果想要互相两个变量的值,需要使用第三个变量作训临时变量,比如:

var a = 10,
    b = 20;

var c = a;
a = b;
b = c;

console.log(a,b)

以上这个C变量对于互相是非常重要的。但是如果使用了es6的数组解构。就不用这个临时变量了。比如:

var a = 10,
    b = 20;
[a,b] = [b,a]
console.log(a,b) // 20 , 10

以上这个案例,赋值语句左侧的解构其它的数组解构的例子是一样的。右侧则是为了互换而临时创建的数组字面量。b和a的值分别被复制给了临时数的第一个和第二个位置。并且进了解约。结果就是两个变量的值进行了互相

和对象解构赋值相同的是,若等号右则的计算结果,为null或undefined,数组解构赋值表达式会发生错误。

5.3.1 数组解构默认值

数组同样允许在数组任意位置指定默认值,如果指定位不存在,或着值是undefined,这个默认值就会被使用.比如:

let users = ["颜苗"];
let [a,b = '赵中',c = '大鹏'] = users
console.log(a,b,c) //颜苗 赵中 大鹏

以上代码中users数组只有一个项,因此并没有为b,c提供匹配的项.然后我们又给他们加了默认值.所以就是对就的名字

5.3.2 嵌套

和对象相似,可以用对象的方式来解构嵌套的数组.也就是可以整个解构中插入另一个数组.就以下这样

let users = ["颜苗",['orange','yellow'],'赵中'];
let [first,[color]] = users
console.log(first,color)

以上first表示users数组中的颜苗,而内层数组orange,包含在里面的一个数组中.因此我们想要拿到它就必须也要加一个方括号[].可以使用任意深度的嵌套.

5.3.3 剩余项

数组解构剩余项,可以使用 … 语法来把剩余项的项目赋值给一个变量,比如:

let users = ["颜苗",['orange','yellow'],'赵中'];
let [first,...arr] = users
console.log(first,arr)  //颜苗 (2) [Array(2), "赵中"]
console.log(arr.length) //2
console.log(arr[0][0]) //orange

以上这个案例,第一个项给了first,剩余项给了一个新的变量arr

剩余项的另一个功能克隆功能。比如:

首先不淡这个剩余项,我们用es5的方法( concat() )来实现克隆

var arr = ['a','b','c'];
var newArr = arr.concat();
newArr.push('d')
console.log(newArr,arr)

尽管concat方法的本意是合并两个数组。但是如果不使用任何的参数来调用这个方法。就会得到个新的数组。但是如果在es6中,我们可以这样做。比如:

let arr = ['a','b','c'];
let [...newArr]  = arr;
newArr.push('d')
console.log(newArr,arr)

以上这个方法,并没有比concat好多少,但是在使用上面也算是一个技巧。

剩余项必须是解构模式中最后的部分,之后不能再有任何的东西。否则就会报语法错误。

5.3.4 混合解构

对象和数组解构被用在一起,用来创建更加复杂的数据形式。大多数时候可以用于ajax数据效果的时候,体现特别的明显。比如:

const  obj = {
    isCheck:true,
    name:"obj",
    local:{
        access:{
           x:100,
           y:300
        }
    },
    news:['尽管concat方法的本意是合并两个数组。但是如','混合解构']
};

let {
    local:{
        access
    },
    news:[firstNews]
} = obj;

console.log(access,firstNews)

以上obj.local.access和obj.news[0]被取出来。并别存给了access和firstnews中。这里需要注意和记住的是解构模式中 local:和 news:只是对应obj 这个对象中属性的位置。混合使用。obj对象的任何部分都能被提出来。对于ajax中返回给我们的json数据来说。 这种方法尤其高效,因为不需要去搜索整个结构.

5.4 参数解构

一个不能被忽略的场景就是,传递函数参数时。当我们的函数接收大量的可选参数时的一个常用的模是创建一个options对象。会在其中包含符加的参数,比如:

// options上的属性表示符加参数
function ajax(url,data,options){
    options = options || {}
    let time = options.time,
        success = options.success;
}

ajax('1.php','张志翔',{
    time:3000,
    success:function(){
        console.log(success)
    }
})

以上这样的案例大家如果去看一些js库的源码,你会看到很多类似这样的代码。在这个ajax函数中,url和data是必须的。而options对象中的time,success则不是必须的。这种方法是有效的,但是无法通过查看函数的定义就能知道我想要输入什么样的代码。必须去阅读开发文档或是源码。

但是用了参数解构之后,可以在第一时间更清楚明白的去标明函数期望输入的参数值。使用对象或数组模式替代具名参数。比如:

function ajax(url,data,{time,success}){
    //其它代码
}

ajax('1.php','张志翔',{
    time:3000,
    success:function(){
        console.log(success)
    }
})

以上这个案例,第三个参数使用了解构来抽取必要的数据。那么现在对于开发者来说,解构之外参数是必须的,而可选的项目放在参数组中。同时如果使用了第三个参数其中应该需要的值,是极期明确。如果没有传值。他们的值就是undefined.

但是如果我们没有给解构传参。在默认情况下,调用函数时未给参数解构传值会报错:比如:

ajax('1.php','张志翔')

在实际调用时,第三个参数的缺失。就相当于undefiend,会导致一个错误,因为参数解构实际上只是解构了声明的简写、当我们的ajax()函数被调用时。js的引擎会修改我们的代码为如下效果

function ajax(url,data,options){
     let {time,success} = options
    //  其它代码
}

因为赋值过程中。右侧的值为null 或是 undefined时。解构会抛出错误,那么也就是未向ajax()函数传递第三个参数时同样也会出错。但是如果我们的要求是可先的。我们可以这样做:

function ajax(url,data,{time,success} = {}){
   console.log('其它代码')
}

ajax('1.php','张志翔')

以上这个案例第三个参数给了一个空对象作为默认值.也就是说没有向函数传递第三个参数.则里面的time,success的值都是undefiend.这时就不会报错.

基于以上的原理,可以为参数解构提供默认值.就和解构赋值时的操作一样.那么也就可以在其中每个参数的后面添加默认值.比如:

function ajax(url,data,{time = 5000,success = true} = {}){
   console.log('其它代码')
}

ajax('1.php','张志翔')

以上代码中参数解构给每个属性都提供了默认值.所以我们可以避免检查指定的属性是否有被传入 .以确保在没有被传入数据的时候,可以使用正确的类型值.而整个解构的参数同样有一个默认值,就是一个空对象,可以让这个参数成为一个可选参数,这个案例稍微要复杂一点.但是可以确保每个参数都有可用的值,而不必加必要检查,所以这样的付出是值得的.

总结

  1. 解构会让我们操作对象和数组时变的更加容易,可以把数据分离并且只拿到你想要的信息.

  2. 对象解构模式可以让你从对象中进行取值,而数组模用于数组.

  3. 对象和数组解构都可以提供默认值.

  4. 特加需要注意的是,如果赋值操作符也就是等号右侧的值为null或undefined的时候,两种模都会报错.

  5. 可以在深层的嵌套中使用解构.可以是任意深度.

  6. 在使用var,let,const,的解构声明创建的变量.必须要提供初始化值.允许我们把值解构到对象属性或已经存在的变量上

  7. 参数解构,可以让选项对象变的更加的清楚透明.

对象的扩展

在ES6当中把对象这块内容做了提升的效用.因为js中基本上所有的值,都是某些类型的对象.

 4.1 对象字面量语法的扩展

对象字面量是js中最常用的模式之一,(json就是基于这种语法.对象字面量的一个普遍性.是因为它的简洁可以少写很多不必要的代码.对 于程序员来说.能少一个字母都是好的.

在我们之前的es5当中,对象字面量必须是名/值对的组合.但是有可能属性值在被始化的时候有些重复.比如

function creFn(name,age){
    return {
        name:name,
        age:age
    }
}

以上属性名和参数名相同,这个结果重复了name和age.在返回的值当中.左边的name是名 右边是值.然后右边的name赋值给了左边的name.

哪么在ES6中.可以更加简洁.比如:

function creFn(name,age){
    return {
        name,
        age
    }
}

当对象中的字面量中的属性只有名称时,js解释器会在周边作用域查找同名的变量.如果找到了,就会把值赋值给同名的属性.

4.2 方法简写

ES6改进了为对象字面量的方法赋值的语法.在早期ES5之前.我们必须指定一个名称并且完整的函数定义来为对象添加方法,比如:

var obj = {
    run:function(){
        return 'hello world'
    }
}

哪么通过es6可以更简洁.比如:

var obj = {
    run(){
        return 'hello world'
    }
}

以上这个案例具有和ES5方法的所有的相同特征和能力,唯一的区别是简写的方法不能使用super,

4.3 需要计算的属性名

需要计算的属性名.其实是在ES5或更加的版本.用于对象实例.只要用方括号表示法来代替小数点表示法就可以.

比如:

var obj = {
    "first name":"虎虎"
}
var lastName = 'last name';
obj["first name"] = '虎';
obj[lastName] = '李';

console.log(obj["first name"])
console.log(obj[lastName])

var lastName = 'lastName';
var obj = {
    "first name":"虎虎",
    [lastName]:"李"
}

console.log(obj["first name"])
console.log(obj[lastName])

// 对象字面量中的方括表示这个属性名需要被计算/其实是一个字符串.也就是说其中可以包含表达式

var su = ' name';
var obj = {
    ["first" + su]:'虎',
    ["last" + su]:'李'
}

console.log(obj["first name"])
console.log(obj['last name'])

4.4 重复的对象字面的属性

ES5的严格模式为重复的字面量引入一个检查.如果重复的属性名.就会报错.

var obj = {
    name:"ddd",
    name:"cccc"   //在es5环境中 strict 模式下不允许一个属性有多个定义 会报错.可以打开IE浏览器进行测试
}

console.log(obj.name)

以上,在ES5严格模式下.第二个name会造成语法错误.,但是在ES6当中移除了重复性的检查.严格和非严格都不在检查.如果存在后面的值就是实际的值.

4.5 正式的方法定义

在ES6之前,方法这个概念在js当中是不存在的.其实叫法是引用了其它语言的叫法,在es6作出了一个正式的定义:方法是一个有homeobject内部属性的函数.此内部属性指向当前方法所属于的对象,比如:

let obj = {
   //这个写法叫方法
   getColor(){
      return 'blue'
   }
}
以下这个写法不是方法.是函数
function getColor(){
    return 'blue'
}

总结

对象是Javascript 编程的中心.做了一些直接的改进.变的更加的强大.主要的改进有,速记法属性定义可以更容易的把作用域内的变量赋值给对象的同名属性;允许被计算,方法的简单写可以省略冒号和function关键字.放弃了同名检查.也就是说我们可以在对象字面量中写两个同名属性值.而不会报错.

函数

3. 函数

函数在任何语言中都是最重要的一部分,没有之一!js函数中.从这门语言诞生到现在…除了遇到了es6之后.发生了一次比较大的化.之前其实都是没有什么变化.但是并不是说完美的…还是有改进的空间.

3.1 在ES5中模拟默认的参数

function ajax(url,type,timeout,callback){
    timeout = timeout || 5000 ;
    type = type || 'get';
    callback = callback || function(){return 1}

    // 其它代码
}

以上这个例子.type和callback 参数是可选的,因为在没有参数的时候,会使用默认值,||或运算符在左侧为假的情况下.返回右侧的内容,因为我们的参数在没有传递的时候返回的是undefined.在逻辑或运算符中用来给缺失的参数提供默认值.但是有一个BUG,因为传进来的参数可能是 0 ,或运算符遇到0为假.所以timeout会被替换成5000

基于以上的情况.我们应该使用一种更加合理更加案例的方法

function ajax(url,type,timeout,callback){
    timeout = (typeof timeout !== 'undefined') ? timeout : 5000

    // 其它代码
}

这种方法,在今天也是比较常见的用法.虽然是安全的..但是代码量过多.而且代码了一个公共的模式.流行的js库中基本上这种写法到处都是..而在ES6当中有更方便的写法,

3.2 es6中的参数默认值

function ajax(url,type = 'get',timeout = 5000,callback = function(){alert(1);}){
    callback()
    // 其它代码
    return {
        url,
        timeout,
        type
    }
}
console.log(ajax('index.html')
console.log(ajax('index.html','post',1000))
console.log(ajax('index.html',undefined,1000))

在以上这个案例中,只有第一个参数需要被传参,其它的都有自己的默认值,这种写法明显比上面的更加的精巧。因为不需要再添加 更多的代码来检查参数值的缺失。

在这个默认参数值的调用中,null是有效的值。而 undefined 则被认为是没有传递参数;

3.3 参数默认值会影响到arguments对象

关于arguments 对象会在使用参数默认值的时候有一些不同的表现,的ES5非严格模式下,在argements 对象会反映出具名参数的变化

function ajax(url,type){
    console.log(url === arguments[0])
    console.log(type === arguments[1])
    url = 'q';
    type = 'w'
    // 其它代码
    console.log(url === arguments[0])
    console.log(type === arguments[1])
}

ajax('url','type')
//以上会得到四个
true
true
true
true

特别需要注意的是,在ES5的严格模式下,这种情况不存在、不会在反映出参数的变化

function ajax(url,type){
    "use strict"
    console.log(url === arguments[0])
    console.log(type === arguments[1])
    url = 'q';
    type = 'w'
    // 其它代码
    console.log(url === arguments[0])
    console.log(type === arguments[1])
}

ajax('url','type')
//以上会得到以下结果
true
true
false
false

在ES6参数默认的函数中。arguments对像的表现总是和ES5的严格模式保持一致。此时无论函数是否运行在严格模式下。参数默认值都会触发arguments对象与具名参数的分离。非常小的一个细节。但是很重要

function ajax(url,type = 'd'){
    console.log(arguments)
    console.log(url === arguments[0])
    console.log(type === arguments[1])
    url = 'q';
    type = 'w'
    // 其它代码
    console.log(url === arguments[0])
    console.log(type === arguments[1])
}

ajax('url')
//以上会得到以下结果
1   //length
true
false
false
false

以上例子中arguments.length的值为1,因为我们只传递了一个参数.也就说是arguments[1]的值是undefined,符合把单个参数传给函数的一个预期值.也就是说url和arguments[0]是相等的.如果去改变url和type 的值不会对arguments对象造成影响.无论是否在严格模式下.都可以始终依据arguments对象来反映初始值调用的状态.

3.4 参数默认值表达式

参数默认值并不要求一定是基本类型值.比如可以接受一个函数产生参数的默认值.比如:

function getValue(){
    return 5;
}
function addValue(number,int = getValue()){
    return number + int
}
console.log(addValue(4))
console.log(addValue(4,87))

以上如果未提供第二个参数…getValue()函数就会被凋用获取表达式的默认值.如果在调用addValue()函数时未提供第二参数时…getValue()才会调用,而在.getValue()的函数声明初次被解析时并不会进行调用.也就意味着.getValue()函数若被写为可变的.则它就有可能返回可变的值.

比如:

let value = 5;
function getValue(){
    return value++;
}

function addValue(number,int = getValue()){
    return number + int
}

console.log(addValue(4))  //9
console.log(addValue(4))  //10
console.log(addValue(4))  //11l

以上就是一个返回可变值的函数的一个体现.因为这样的一种行为.有一个比较有意思的现像,可以把前面参数作为后面参数的默认值

function addValue(number,int = number){
    return number + int
}

console.log(addValue(4,4)) 
console.log(addValue(4)) 

以上案例中number为int提供了默认值.意味只要传入一个参数就会让两个参数获得相同的值.如果是这样的话.哪我们的这个number,是不是可以作为第二个默认参数函数的形参.

function getValue(value){
    return value + 5;
}

function addValue(number,int = getValue(number)){
    return number + int
}

console.log(addValue(4,4))  //8
console.log(addValue(4))    //13

以上案例把number的值设为getValue()函数的返回值.因此addValue(4)会返回(4 + 9),而另一个依然是4+4.

引用其它参数 作为参数进行默认赋值时,只能引用前方的参数,因此前面的参数不能访问后面的参数.比如:

function addValue(number = int,int){
    return number + int
}

console.log(addValue(undefined,1))   //报错

因为int 在number 之后定义,不能把它作为后者的默认值..存在暂时性死区.

3.5 剩余参数

剩余参数 rest parameter由三个点( … )与一个具名参数指定.会包含传递给函数的其余参数的一个数组.名称中的剩余由此而来.比如

function ajax(obj,...keys){
   for(let i=0,length = keys.length;i<length;i++){
       //其它代码
   }
}

keys是一个包含所有在obj之后的参数的剩余参数(这和包含的有的参数的 arguments 不同,因为arguments 会把第一个参数也包含在内).也就是说我可以去对keys进行从头到尾的遍历.

如果你用了剩余参数,就必须把剩余参数放在最后.一个函数中只能有一个剩余参数.

function ajax(obj,...keys,last){
   for(let i=0,length = keys.length;i<length;i++){
       //其它代码
   }
}

以上代码会发生语法错误,不能在剩余参数之合使用具名参数,同样的剩余参数也不能用于对象字面量中的set属性中使用,比如

let obj = {
    // 不能在setter中使用剩余参数
    set abc(...value){

    }
}

3.6 箭头函数

箭头函数算是一个比较明显改变, 使用(=>)来定交箭头函数.在行为上面和传统函数不同.

  1. 没有this,super,arguments,也没有所谓的new.target梆定
  2. 不能被new 调用.也就是如果你要写构造函数不能使用箭头函数.
  3. 没有原型,既然不能对箭头函数使用new 那么也就不需要原型.也就没有prototype属性
  4. 不能改变this.this的值在函数内部不能被修改/在 函数的整个生命周期内其值保持不变
  5. 没有arguments,箭头函数没有和arguments绑定.就必须依赖于具名参数或剩余参数来访问函数的参数
  6. 不允许重复的具名参数,无论是否在严格模式下都不能允许.而对于传统函数来说.只有在严格模式下才禁止这种重复.

 3.6.1 语法

箭头函数的语法有比较多的写法,怎么写取决于你想要完成什么样的目标.所有的写法都以函数参数为开头

后面跟着是箭头,接下来的是函数体.

var res = value => value;
//等价于以下
var res = function(value){
     return value
}

当箭头函数只有单个参数时..这个参数可以直接书写.不需要其它额外的语法.箭头右边的表达式,会被计算并返回结果.

如果要参入多个参数,就要放在一个括号内

var sum = (n,m) => n + m;
// 等价于以下
var sum = function(n,m){
  return n + m;
}

以上这个案例和上面的案例唯一的区别就是参数在括内.相互之间和传统函数一样.用逗号隔开.如果说函数没有任何参数.那必就必须使用一对空括号

var sum = () => '李虎';
// 等价于以下
var sum = function(){
  return '李虎'
}

如果说想要有传统的函数体,也可以包含多个语句的时候,用一对花括号包进来.然后也就以使用return,比如:

var sum = (n,m)  => {
    return n + m;
}
// 等价于以下

var sum = function(n,m){
  return n + m;
}

基本上坷以把花括号内的代码当成传统函数对待就可以,除了不能使用 arguments 如果说想要创建一个空函数;

var sum = ()  => {}
// 等价于以下
var sum = function(){}

花括号表示函数的主体,所有的正常的工作都必须放在花括号当中.如果箭头函数想从函数体内向外返回一个对象字面量.就必须把字面量包括在圆括内;

var getOBJ = id => ({id:id,name:"李虎"})

// 等价于以下

var getOBJ = id => {
    return {
        id:id,
        name:"李虎"
    }
}
var getOBJ = function(id){
    return {
        id:id,
        name:"李虎"
    }
} 

以上把对象字面量包括在括号内.表示了括号内是一个字面量而不是函数体.

3.6.2 创建立即调用函数表达式

我们在使用函数的一种流行的方式 创建立即调用函数表达式 (IIFE: Immediately Invoked Function Expression)

IIFE允许我们自定一个匿名函数并在未保存引用的情况下立即调用它.比如:

let obj = function(name,age){
    return {
        getName:function(){
            return {
                name,
                age
            }
        }
    }
}('李虎',22)

console.log(obj.getName())

在以上案例中IIFE 被用于创建一个包含 getName() 方法的对象.有两个参数作为返回值.可以有效的让为两个返回值(name,age)成为返回对象的一个私有成员. 我们可以使用箭头函数来完成同样的事 情.只要把它包括在括号内就可以.

let obj = ((name,age)=>{
    return {
        getName:function(){
            return {
                name,
                age
            }
        }
    } 
})('李虎',22)

console.log(obj.getName())

这里需要注意的是括号仅仅包含了箭头函数的定义.并没有包括(‘李虎’,22),这区别于传统函数的方式.括号即可以连函数定义与参数一起包含.也可以只包含函数定义.

3.6.3 没有 this 绑定

JavaScript中最常见的错误就是在函数内的this绑定,因为在一个函数内部的this值可以被改变.这个改变的过程.取决于在调用这个函数时的上下文关系,就可能错误的影响一个对象,尽管本来是想要修改另一个对象的..但是结果可能并不是,比如:

var handler = {
    id:"53853",
    init:function(){
        document.addEventListener('click',function(ev){
             this.ido(ev.type) //报错了 this.ido is not a function
        },false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()

handler 对象作用是用于设计处理页面上的交互,在调用init()方法,用于建立交互关系.用于和当前页面进行一次行为对话,并且还添加了一个事件处理函数this.ido().但是在实际调用过程中,this.ido() 发生了错误.告诉我们这不是一个方法.不是一个function,这是因为this是事件目标对象,也就是当前案例中的(docuemnt),而不是被绑定在handler对象上.所以在运行此代码时..会触发一个错误.因为this.ido()这个方法并不存在于document对象上.

在箭头函数之前解决此类问题.可以使用bind()方法把函数的this.绑定到handler对象上.以修正这段代码:示例如下

var handler = {
    id:"53853",
    init:function(){
        document.addEventListener('click',(function(ev){
            this.ido(ev.type) //已经是正确的
       }).bind(this),false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()

以上案例.通过调用bind(this),实际上是创建了一个新函数,它的this绑定到当前的this也就是 handler 对象之上.如果想要避免创建一个函数.最好的方式.不是下面的在之前存一个对象.而是使用箭头函数

也可以在在事件对象之前保存this对象

var handler = {
    id:"53853",
    init:function(){
        var self = this;
        document.addEventListener('click',function(ev){
            self.ido(ev.type) //已经是正确的
       },false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()

在使用箭头函数时.因为没有this绑定,也就意味着.箭头函数内部的this值.只能通过查找作用域链的方式来确定我的当前对象.如果箭头函数被包含在 一个非箭头函数中.哪么this就会和这个函数相等;否则this就会是全局对象也就是window.如果是在node.js当中的是global,下面我们用箭头函数再次修正此代码,示例如下:

//示例1
var handler = {
    id:"53853",
    init:function(){
        document.addEventListener('click',ev=>this.ido(ev.type),false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()
//示例2
//也可以这样写
//效果同上,只是箭头函数的写法有所区别
var handler = {
    id:"53853",
    init:function(){
        document.addEventListener('click',(ev)=>{
            this.ido(ev.type)
        },false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()

以上案例中事件处理函数是一个调用this.ido的箭头函数。。它的this和init()方法相同。所以这个版本代码工作方式类似于我们前面使用bind(this)的案例。尽管this.ido()方法并不返回何值。但依然是函数内体唯 一能被执行的语句。因此无须使用第二个案例用花括号来包含它。但是我依然推荐大家使用第二种,读性更高一点。

箭头函数不用被用于定义新的类型。没有prototype,所以不能使用 new 运算符。如果使用会报错。从这点上可以看出箭头函数。在设计之设就是属于抛弃型函数。

var Fn = ()=>{};
fn = new Fn()  //Uncaught TypeError: Fn is not a constructor

以上代码中报错了,因为是一个箭头函数,它不存在constructor方法,所以箭头函数不能被用于new

同样,由于箭头函数中的this由包含它的函数决定,因此也不能使用call(),apply()或bind()方法来改变其值。

3.6.4 箭头函数和数组

比如如果你想使用自定度的比较器来数组进行排序。我们可以这样写

传统的写法:

 var result = arr.sort(function(a,b){
    return a - b;
 })

以上是传统的写法,如果使用箭头函数,可以有更简洁的书写体验

var result = arr.sort((a,b)=>a-b);

还可以使回调函数的数组方法,比如map() 或reduce().都可以把看着复杂的需求转换成更加简单明了的代码。

3.6.5 没有arguments绑定

箭头函数没有自己的arguments的绑定。但是依然可以访问包含它的函数的arguments对象。因此箭头函数无论在何处被执行。这个对象都是可以用的。比如:

function createFn(){
    return () => arguments[0];
}
var fn = createFn(5)
console.log(fn())  //5

以上案例在createFn()内部,arguments[0]被已经创建的箭头函数所引用。这个引用包含了传传递给createFn()函数的首个参数,因此当箭头函数在执行之后。可以反回5,这是因为传给createFn()的首个参数,尽管箭头不存在arguments。但是但是创建他的函数的作用域内是存在的。由于作用域链的关系。依然可以被访问。

识别箭头函数

尽管箭头函数和传统函数.语法不同.但是依然是函数.这个本质不能搞乱.也能被正常的识别

比如:

var fn = (a,b)=>a-b;
console.log(typeof fn)  //function
console.log(fn instanceof Function)  //true

以上两条console.log()的结果.可以明确的知道,typeof 和 instanceof 在箭头函数中的行为和作用与其它的函数完全一样.所以你依然可以对箭头函数使用call,apply和bind(),虽然函数的this绑定不会受影响.

比如:

var sum = (num1,num2) => num1 + num2;
console.log(sum(1,1))
console.log(sum.call(null,1,2))
console.log(sum.apply(null,[1,2]))
var bindSum = sum.bind(null,1,2);
console.log(bindSum())

以上案例sum()被使用call()和apply()方法.并传了参数,bind()方法用于创建一个新函数bindSum().

总结

可能对于很多有经验的开发者来说.并不会感受到箭头函数有什么革命性的变化.事实上变化也不是我们看到的这么大.只是增加了一些增量改进.让函数更加容易使用而已.

npm 教程

简介

npm,全名 node package manger。

  • npm 是Node的开放式模块登记和管理系统,是Node.js包的标准发布平台,用于Node.js包的发布、传播、依赖控制,网址:https://www.npmjs.com/
  • npm 提供了命令行工具,可以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包

npm 如何使用

  • npm 在按照 Node.js 时会连带被安装。但有可能不是最新版本,需要 npm install npm@latest -g 升级到最新版本。

基本命令:

# 查看 npm 命令列表
$ npm help

# 查看各个命令的简单用法
$ npm -l

# 查看 npm 的版本
$ npm -v

# 查看 npm 的配置
$ npm config list -l

npm 的使用

npm init 初始化 package.json 文件

用来初始化生成package.json文件。在这个过程中会向用户提问一系列问题,如果你觉得不用修改默认配置,一路回车就可以了。

如果使用了-f(代表force)、-y(代表yes),则跳过提问阶段,直接生成一个新的package.json文件。

npm set 设置环境变量

  $ npm set init-author-name 'Your name'
  $ npm set init-author-email 'Your email'
  $ npm set init-author-url 'http://yourdomain.com'
  $ npm set init-license 'MIT'

上面命令等于为npm init设置了默认值,以后执行npm init的时候,package.json的作者姓名、邮件、主页、许可证字段就会自动写入预设的值。这些信息会存放在用户主目录的~/.npmrc文件,使得用户不用每个项目都输入。如果某个项目有不同的设置,可以针对该项目运行npm config

  $ npm set save-exact true

上面命令设置加入模块时,package.json将记录模块的确切版本,而不是一个可选的版本范围。

npm config

  $ npm config set prefix $dir

上面的命令将指定的$dir目录,设为模块的全局安装目录。如果当前有这个目录的写权限,那么运行npm install的时候,就不再需要sudo命令授权了。

$ npm config set save-prefix ~

上面的命令使得npm install --savenpm install --save-dev安装新模块时,允许的版本范围从克拉符号(^)改成波浪号(~),即从允许小版本升级,变成只允许补丁包的升级。

$ npm config set init.author.name $name
$ npm config set init.author.email $email

上面命令指定使用npm init时,生成的package.json文件的字段默认值。

npm info

npm info命令可以查看每个模块的具体信息

$ npm info underscore
$ npm info underscore description
$ npm info underscore homepage
$ npm info underscore version

npm search

npm search命令用于搜索npm仓库,它后面可以跟字符串,也可以跟正则表达式

  $ npm search <搜索词>

npm list

npm list命令以树型结构列出当前项目安装的所有模块,以及它们依赖的模块。

npm list
npm list -global
npm list vue

加上global参数,会列出全局安装的模块。

npm install

Node模块采用npm install命令安装。

每个模块可以“全局安装”,也可以“本地安装”。“全局安装”指的是将一个模块安装到系统目录中,各个项目都可以调用。一般来说,全局安装只适用于工具模块,比如eslintgulp。“本地安装”指的是将一个模块下载到当前项目的node_modules子目录,然后只有在项目目录之中,才能调用这个模块

# 本地安装
$ npm install <package name>

# 全局安装
$ sudo npm install -global <package name>
$ sudo npm install -g <package name>

# 也支持直接输入Github代码库地址
$ npm install git://github.com/package/path.git
$ npm install git://github.com/package/path.git#0.1.0

# 强制重新安装
$ npm install <packageName> --force

# 如果你希望,所有模块都要强制重新安装,那就删除node_modules目录,重新执行npm install
$ rm -rf node_modules
$ npm install

安装不同版本

install 命令总是安装模块的最新版本,如果要安装模块的特定版本,可以在模块名后面加上@和版本号。

$ npm install sax@latest
$ npm install sax@0.1.1
$ npm install sax@">=0.1.0 <0.2.0"

# 如果使用--save-exact参数,会在package.json文件指定安装模块的确切版本
$ npm install readable-stream --save --save-exact

$ npm install sax --save
$ npm install node-tap --save-dev
# 或者
$ npm install sax -S
$ npm install node-tap -D

# 如果要安装beta版本的模块,需要使用下面的命令
# 安装最新的beta版
$ npm install <module-name>@beta (latest beta)
# 安装指定的beta版
$ npm install <module-name>@1.3.1-beta.3

# npm install默认会安装dependencies字段和devDependencies字段中的所有模块,如果使用--production参数,可以只安装dependencies字段的模块
$ npm install --production
# 或者
$ NODE_ENV=production npm install

避免系统权限

默认情况下,Npm全局模块都安装在系统目录(比如/usr/local/lib/),普通用户没有写入权限,需要用到sudo命令。这不是很方便,我们可以在没有root权限的情况下,安装全局模块。

首先,在主目录下新建配置文件.npmrc,然后在该文件中将prefix变量定义到主目录下面。

  prefix = /home/yourUsername/npm

然后在主目录下新建npm子目录

$ mkdir ~/npm

此后,全局安装的模块都会安装在这个子目录中,npm也会到~/npm/bin目录去寻找命令。

最后,将这个路径在.bash_profile文件(或.bashrc文件)中加入PATH变量。

export PATH=~/npm/bin:$PATH

npm update

npm update命令可以更新本地安装的模块

# 升级当前项目的指定模块
$ npm update [package name]

# 升级全局安装的模块
$ npm update -global [package name]

它会先到远程仓库查询最新版本,然后查询本地版本。如果本地版本不存在,或者远程版本较新,就会安装。

使用-S--save参数,可以在安装的时候更新package.json里面模块的版本号。

注意,从npm v2.6.1开始,npm update只更新顶层模块,而不更新依赖的依赖,以前版本是递归更新的。如果想取到老版本的效果,要使用下面的命令。

$ npm --depth 9999 update

npm uninstall

npm uninstall命令,卸载已安装的模块

$ npm uninstall [package name]

# 卸载全局模块
$ npm uninstall [package name] -global

npm run

npm 不仅可以用于模块管理,还可以用于执行脚本。package.json 文件有一个 scripts 字段,可以用于指定脚本命令,供npm直接调用。

npm run 命令会自动在环境变量 $PATH 添加 node_modules/.bin 目录,所以 scripts 字段里面调用命令时不用加上路径,这就避免了全局安装 NPM 模块。

npm run 如果不加任何参数,直接运行,会列出 package.json 里面所有可以执行的脚本命令。

npm内置了两个命令简写,npm test 等同于执行 npm run test,npm start 等同于执行 npm run start。

$ npm i eslint --save-dev

pre- 和 post- 脚本

npm run 为每条命令提供了 pre- 和 post- 两个钩子(hook)。以 npm run lint 为例,执行这条命令之前,npm会先查看有没有定义 prelint 和 postlint 两个钩子,如果有的话,就会先执行 npm run prelint,然后执行 npm run lint,最后执行npm run postlint。

全局模块(在命令行的任何地方可以使用) 局部模块

  • 为什么全局模块可以直接在任何地方使用

全局的模块

  • 必须使用package.json 中配置bin参数

  • ! /usr/bin/env node

npm link

就是将当前的目录临时的放到全局下。

开发 NPM 模块的时候,有时我们会希望,边开发边试用,比如本地调试的时候,require(‘myModule’) 会自动加载本机开发中的模块。Node规定,使用一个模块时,需要将其安装到全局的或项目的 node_modules 目录之中。对于开发中的模块,解决方法就是在全局的 node_modules 目录之中,生成一个符号链接,指向模块的本地目录。

npm link 就能起到这个作用,会自动建立这个符号链接。

请设想这样一个场景,你开发了一个模块 myModule,目录为 src/myModule,你自己的项目 myProject 要用到这个模块,项目目录为 src/myProject。首先,在模块目录(src/myModule)下运行 npm link 命令。

src/myModule$ npm link

上面的命令会在NPM的全局模块目录内,生成一个符号链接文件,该文件的名字就是 package.json 文件中指定的模块名。

/path/to/global/node_modules/myModule -> src/myModule

这个时候,已经可以全局调用 myModule 模块了。但是,如果我们要让这个模块安装在项目内,还要进行下面的步骤。

切换到项目目录,再次运行 npm link 命令,并指定模块名。

src/myProject$ npm link myModule

上面命令等同于生成了本地模块的符号链接。

src/myProject/node_modules/myModule -> /path/to/global/node_modules/myModule

然后,就可以在你的项目中,加载该模块了。

var myModule = require('myModule');

这样一来,myModule 的任何变化,都可以直接反映在 myProject 项目之中。但是,这样也出现了风险,任何在myProject目录中对myModule的修改,都会反映到模块的源码中。

如果你的项目不再需要该模块,可以在项目目录内使用 npm unlink 命令,删除符号链接。

src/myProject$ npm unlink myModule

npm bin

# 项目根目录下执行
$ npm bin
./node_modules/.bin

npm adduser

$ npm adduser
Username: YOUR_USER_NAME
Password: YOUR_PASSWORD
Email: YOUR_EMAIL@domain.com

npm publish

npm publish用于将当前模块发布到npmjs.com。执行之前,需要向npmjs.com申请用户名。

# 需要向npmjs.com申请用户名
$ npm adduser

# 登录
$ npm login

# 发布
$ npm publish

# 如果当前模块是一个beta版,比如1.3.1-beta.3,那么发布的时候需要使用tag参数,将其发布到指定标签,默认的发布标签是latest
$ npm publish --tag beta

# 如果发布私有模块,模块初始化的时候,需要加上scope参数。只有npm的付费用户才能发布私有模块。
$ npm init --scope=<yourscope>

# 如果你的模块是用ES6写的,那么发布的时候,最好转成ES5。首先,需要安装Babel。
$ npm install --save-dev babel-cli@6 babel-preset-es2015@6

然后,在package.json里面写入build脚本。

"scripts": {
  "build": "babel source --presets babel-preset-es2015 --out-dir distribution",
  "prepublish": "npm run build"
}

运行上面的脚本,会将 source 目录里面的ES6源码文件,转为 distribution 目录里面的 ES5 源码文件。然后,在项目根目录下面创建两个文件 .npmignore 和 .gitignore,分别写入以下内容。

// .npmignore
source

// .gitignore
node_modules
distribution

npm deprecate

如果想废弃某个版本的模块,可以使用 npm deprecate 命令。

$ npm deprecate my-thing@"< 0.2.3" "critical bug fixed in v0.2.3"

github / npm

npm版本号管理的问题

  • semver规范 规定了版本号 由3位组成 MAJOR MINOR PATCH
    • MAJOR 可能不在兼容老版本
    • MINOR 新增了一些兼容旧版本的api vue.observable
    • PATCH 修复bug

都是git的tag 对应着npm的版本

npm version major minor patch

会自动和git进行关联

版本号含义

  • 2.2.0 必须是2.2.0

  • ^2.2.0 限定大版本,后面更新只要不超过2尽可以

  • ~2.2.0 限定前两个版本,后面的版本只要比0大就可以

  • =2.0 大于这个版本

  • <=2.0

  • 1.0.0 – 2.0.0

预发版本

  • alpha 预览版 内部测试版
  • beta 测试版 公开测试版
  • rc 最终测试版本

scripts

  • 可以配置脚本的命令 快捷键(可以把很长的命令放到scripts中)
  • 执行命令 会将当前的node_modules目录下的.bin文件夹放到全局中(所以可以直接使用)
  • npm run start 可以简写成 npm start

npx

  • npx和script一致可以帮我们直接运行 .bin目录下的内容
  • 如果.bin目录下存在 会执行对应脚本,如果不存在会下载运行

npx 只是一个临时的使用方案。 npm5.2 之后产生的

源的切换 (npm nrm nvm)

  • npm install nrm -g
  • nrm ls / nrm use

包的发布

  • 如何发布一个包 先注册npm账号
  • 一定要在官方源上发
  • npm addUser 添加用户
  • npm publish 发布包

模板字面量

2. 模板字面量

  1. es6之前.想实现多行字符串,要么使用一个原来js,语言级bug来实现一个多行.
  2. 把字符串替换已存在的变量值的能力
  3. HMTL转义

2.1 基本语法

使用一个反引号 “ 用来包含普通字符串,而不是之前的双引号或单号

let msg = `hello ES6`;
console.log(msg)

在以上这 个案例中,并没有感受到和之前有什么不一样.

如果想要输出反引号 “ 可以使用 转义符 \

let msg = `\`hello ES6\``;
console.log(msg)

在模板字符串里面.如果想要输出单引号或双引号.就不需要转义符 \

let msg = `"hello"'ES6'`;
console.log(msg)

2.1 多行字符串

在ES6之前想要实现多行字符串.可以使用一个语法BUG,在换行之前使反斜线 ( \ ),可以用于创建多行字符串.为什么不用.因为是一个语言级的BUG.你不知道什么时候他被修复了.

var msg = "hello \
ES6";
console.log(msg)

这里确实换行了.并且可以被输出.但是打印的时候并不会真正的换行.因为反斜线 ( \ )是延续符号,而不是新行的符号.如果你想要在输出中换行.可以这样写

var msg = "hello \n\
ES6";
console.log(msg)

ES6中推出的多行字符串就不需要使用这种BUG了只需要在你需要的地方包含换行就可以了

let msg = `hello 
es6;
`
console.log(msg)

需要注意提所有的空格都会被认为是内容的一部分,所以需要注意空格的问题.保持合理的缩进

let ouput = `

<h1>
    <div>hello</div>
</h1>

`.trim();
console.log(ouput)

trim()可以清空字符串左右两边的空格.

如果只是单纯想要实现输出结果的换行可以使 \n ;

以上只是可以实现换行…但是真正的意见在于允许你使用js表达式嵌入到模板字面量当中.

比如:

let msg = 'hello';
let res = `${msg},world`;

console.log(res)

替换位 \${msg} 替换位是\$开始,与结束{}来界定.之间可以放入任意的JS表达式.以上是最简单的示例

既然替换位是表达式.哪么可替换的就不可能只是变量.还可以放计算,函数调用.

let n = 5,
    m = 20.452342,
    msg = `${n+m}`;

    msg = `$${(n * m).toFixed(2)}`
console.log(msg)

在上面这个案例中.我可使用了计算.并且调用方法.在替换位之前的美元符号可以被正常输出.因为没{}跟随其后.

另外模板字面量本身也是JS表达式,这也就是可以把模板字面量嵌入到另一个模板字面量的内部

比如:

let msg = "张三",
    res = `hello ${
        `他叫${msg}`
    }`;
    console.log(res)

现在我们都已经理解 了字面量不用做任何的连接…就可以直接使用多行字符串以膝上计算机插值功能.不过这个还不是真正牛逼的地方..真正厉害的是模板标签(template tag),能对模板字面量进行转换并返回最终的字符串值.标签在模板起始处被指定. 也就是在第一个` 之前

比如

let msg = tag`hello 张三`;

在上面这个例子,tag就是会被应用到hello 张三,

一个标签tag仅仅只是一个函数,在被调用 时接收需要处理的模板字面量的数据.标签所接收的数据被划分为独立的片段,并且必须组合来创建结果.第一个参 数是一个数组,包含被js解释过的字面量字符串.随后的参数是每个替换位的解释值

function tag(val,...args){
// 返回一个字符串
}

用一个列子来解释一下传递给标签的是什么参数.

比如:

let conut = 10;
let price = 0.25;
let msg = tag`${conut} items cost $${(conut * price).toFixed}.`;

如果我有一个tag()的函数.这个函数会接受到三个参数,第一个是一个数组 arr.会包含以下的元素

  1. 在首个替换位之的空字符串 ""
  2. 首个替换位与第二个替换位之间的字符串 items cost $
  3. 第二个替换位之后的字符串 "."

第二个参数会是10, 也就是 ${conut} 变量的解释值. 会成为 args 数组的第一个元素.

第三个也就是最后一个,会是 ${(conut * price).toFixed()} 的解释值 2.50,并且是args数组的第二个元素

示例如下:

["","items cost $","."]
[10,2.50]

需要注意的是 第一个参数数组的第一个元素是一个空字符串,以确保arr[0],总是在字符串的起始分,

也就是正如 arr[arr.length – 1] 总是是字符串的结尾部分.同时替换的元素数量也总比字符量元少1

就意味着,args.length === arr.length-1的值是 true;

在当前这种情况下,,可以交替使用arr 和 args 数组来创建一个结果字符串;

用arr中首个元素开始。。。后面紧跟着args的首个元素。。以此反复。直到结果字符串被全部创建完毕

function fn(arr,...args){
    let result = "";
    // 直接使用args的元素数量来进行循环
    for(let i=0;i<args.length;i++){
    result += arr[i];
    result += args[i];
}
    result += arr[arr.length - 1]
    return 1;
}

let conut = 10;
let price = 0.25;
let msg = fn`${conut} items cost $${(conut * price).toFixed()}.`;

console.log(msg)

在以上这个例子中定义了 fn 标签,能够执行与模板字面量的默认行为相同的换操作,唯一的需要注意的是在循环中使用args.length,而不是arr.length用来避免args的数组越界,这是冂es6对 arr 和 …args 一个定义;

…args中包含的值不必是字符串,也可以是计算后的结果.

ES6 块级绑定

1. 块级绑定

基本上在类C语言中,变量的创建总是在它被声明的地方.但javascript并非如此.变量实际的位置是取决于开发着如何声明它..基于这样的一个存在,ES6闪亮登场.提供了一个let,可以让你更容易更直观的控制变量的作用域.

1.1. 所谓的变量提升

如果我们使用var 声明变量.如果在函数内.不管我写在什么地方,都会被视为写在函数顶部.如果不在函数内.则视为全局作用域的顶部.就是所谓的变量提升

function getVal(cond){
    if(cond){
        var color = 'blue';
        return color;
    }else{
        // color 在此处可以被访问 值为 undefined
        return null
    }
}

以上代码在js解释器中会为被修改为

function getVal(cond){
    var color
    if(cond){
        color = 'blue';
        return color;
    }else{
        // color 在此处可以被访问 值为 undefined
        return null
    }
}

为了解决这个BUG..引入了块级作用域

块级声明也就是让所声明的变量在指定的块的作用域外无法被访问.块级作用域(词法作用域)

  1. 在一个函数内部
  2. 在一个代码块内部 {}

块级作用域是类C语言的工作机制.

1.2. let 声明

使用方法和var语法一致,基本上你可以用let来代替var进行变量声明,但是会把变量的作用域限制在当前的代码块当中.如果想要提升,需要手动操作
以下示例:

function resu(val){
    if(val){
        let color = 'blue';
        return color;
    }else{
        // color 在此处  color is not defined
        return color
    }
}

function resu(val){
    let color 
    if(val){
        color = 'blue';
        return color;
    }else{
        // color 在此处可以被访问 值为 undefined
        return color
    }
}

console.log(resu(0))

1.3. 禁止重复声明

如果一个标识符已经被定义,就不能在此代码块中使用同一个标识符进行 let 声明,否则会报错

var number = 0;
let number = 1;   // Uncaught SyntaxError: Identifier 'number' has already been declared

在同一作用域下不能得复声明
但是在嵌套的作用域内使用同名的新变量.就不会所错

var number = 10;
if(true){
    let number = 40;
}

以上案例中.let 并没有报错,因为在if条件语句中创建新变量number.而不是在同一级别再次创建.在if代码块中会屏蔽全局的number变量.从而在局部组止对于后者的访问.

1.4. 常量声明

声明一个常量可以使用const语法进行声明.常量意味声明之后的值不可变.所以所有的常量都要在声明的时候初始化.

// 有效的常量
const MAXNUMBER = 300// 语法错误的常量
const NAME;

以上MAXNUMBER这个常量初始化并且赋值了.因此是正常而有作用的.而NAME常量没有被赋值.会导致语法误法;
const和let声明一样都是块级声明.有声明也不会有提升的情况发生

if(true){
    const NUMBER = 40;
}
// NUMBER 在此处无法被访问

已经被声明过的变量.不可以用常量再次声明

var msg = 0;
let height = 190;

// 以下两者都会报错
const msg = '1';
const height = 170;

常量不可被改变

const height = 170;
height = 180;  //Uncaught TypeError: Assignment to constant variable.

常量确定之后.不可以再次被赋值

如果使用const 声明对象.
const声明是会阻止对于常量绑定与常量自身值的修改,也就是说并不会去阻止对于常量的成员的修改;

const obj = {
    name:"张三"
}  //
obj.name = '李四';
// 报错  Uncaught TypeError: Assignment to constant variable.
obj = {
    name:"赵五"
}
// 数组也是一样的
const arr = [];
arr.push(1)
arr.push(1)
console.log(arr)

此处对象初始化的时候有一个属性,修改属性是可以,但是不能修改对象本身.只需要记住const阻止的是对于常量绑定的修改.而不是对于成员值的修改

1.4. 暂时性死区

使用let , const 声明的变量,在达到声明之前.都是无法访问的.就算是相对安全的操作(比如typeof),也是一样的

console.log(typeof abc)
let abc;

以上abc 变量使用了let 进行定义,但是这条语句.永远不会被执行.因为在声明之前就已经报错了.出现这个问题是因为暂时性死区.但是暂时性死区是 js 社区(比如网上论坛)提出的,这个名称从来没有在ECMAscript规范中被明确命名.但是经常用于描述let或const声明 的变量为何在声明处之前无法被访问

1.5. 循环中的块级绑定

我们最需要使用的变量的块级场景是,也许是for,也就是想让一次性的循环计数只能在内部使用
以下示例应该是最常见的一种行为

for (var index = 0; index < 10; index++) {}
console.log(index)   //10

但是在大多数语言中.以上index.只能在循环内工作访问.这里的index在外部可以被访问.是因为使用var声明导致了变量提升,如果换成let,就不会发生

for (let index = 0; index < 10; index++) {}
console.log(index)   //app.js:8 Uncaught ReferenceError: index is not defined

在以上案例中.index仅在for内部使用. 循环结束..这个变量的任意位置不可被访问

1.6. 循环内的函数

在let没有出现之前,因为var的特点.会使循环变量作用域之外还可以被访问,于是在循环内创建函数会有问题.

var arr = [];
for(var i=0;i<10;i++){
    arr.push(function(){
        console.log(i)
    })
}

arr.forEach(function(fn){
    fn()
})

我们本身的预期是希望输出0-9的数字,但是实际结果是数字10被输出了10次.

我们在修正这个问题的时候可以使用IIFE(立即调用函数表达式),就可以在每次迭代中强制性的创建变量的一个新的副本.

var arr = [];
for(var i=0;i<10;i++){
    arr.push((function(value){
        return function(){
            console.log(value)
        }
    })(i))
}

arr.forEach(function(fn){
    fn()
})

上面在循环内使用IIFE,变量 i 被传递给了IIFE, 去创建了value变量为自身的副本,并在自己当中

1.7. 循环内的let

使用let声明.可以简化IIFE.在每交迭代中.都创建一个新的同名变量,对其进行初始化.

var arr = [];
for(let i=0;i<10;i++){
    arr.push(function(){
        console.log(i)
    })
}

arr.forEach(function(fn){
    fn()
})

以上代码完全实现了相同的效果.但是更加简洁.效果更好,同样的方式在for-in 和 for-of中同样适用.

var arr = [];
var obj = {
    name:"张三",
    sex:"",
    age:22
}

for (let key in obj){
    arr.push(function(){
        console.log(key)
    })
}

// console.log(arr)

arr.forEach(function(fn){
    fn()
})

在以上for-in循环中体现出for相同的行为.

let声明在循环内部的行为是在规范中特别被定义过的.而与不提升变量的特征没有必然的联系.事实上在let刚刚出来的时候是没有这种行为的…在后面不停变更过程中加进来的.

1.8. 循环内的常量

ES6规范没有明确禁止在循环中使用常量.然而它会根据循环方式的不同有不同的行为体现.在常规的for当中.可以使用const初始化.但是如果你去循环了.就等于是想要改变它.就会报错

for (const index = 0; index < 10; index++) {
    console.log(index)
}

代码会执行一次.但是第二循环的时候,试图改变index.这个时候就报错了.

但是如果是在for-in或是for-of.效果和let完全相同.

var arr = [];
var obj = {
    name:"张三",
    sex:"",
    age:22
}

//不会报错
for (const key in obj){
    arr.push(function(){
        console.log(key)
    })
}

arr.forEach(function(fn){
    fn()
})

因为每次迭代创建一个新的变量绑定.而不是试图去修改已经绑定的变量的值.所以就不会报错

1.9. 全局的块级绑定

let和const不同于var的另一个区别就是在全局作用域上的表现.在全局作用域上使用var,会创建一个新的全局变量.并且成为全局对象(window)的一个属性.这也就是竟味使用var可能会无意中覆盖一个已经有的全局属性,

var RegExp = 'h';
console.log(window.RegExp)  //h

RegExp定义在window.但还是被重写了.但是如果使用了let 或 const,虽然在全局中会创建新的绑定,但是不会有任何属性被添加到了全局对象上去.也就意味着不用使用let或const来覆盖一个全局变量.只能把他屏蔽.

let RegExp = 'h';
console.log(RegExp// h
console.log(window.RegExp//ƒ RegExp() { [native code] }
console.log(window.RegExp === RegExp)   //false

如果希望代码能从全局对象中访问.依然需要使用var.在浏览器跨窗口去访问代码时适用.

在拥有了let和const之后..默认情况下应该使用let而不是var 对于90%以上开发者来说let的行为才是正确的行为.是声明变量本来就应该有形式.所以直接使用let更合逻辑.在需要被保护的变量时使用const.

1.10. 总结

  1. let或const有块级作用域
  2. var 只有全局和函数作用域
  3. let 不能重复声明,const也不能重复声明
  4. 如果使let 或 const 必须在使用声明.如果在使用后声明,会产生暂时性死区
  5. 可以解决循环中的闭包的问题
  6. var在全局下声明的变量,会挂载到window下面
  7. let或const会挂载到window下面.