深入理解ES6 - Nicholas C.Zakas

avatarplhDigital nomad

第一章 块级作用域绑定

var 会有一个作用域提升的问题

console.log(a)    // undefined
var a = '123';

等同于

var a;
console.log(a)    // undefined
a = '123';

等同于

let a;
console.log(a);
a = '123';

变量a被提升到顶部,因为这种bug,let,const等块级作用域诞生了。 let/const 的声明周期和var不一样。

块级声明

块级作用域(同时被称为词法作用域)存在于

  • 函数内部
  • 块中 {}[]
var的变量提升,原本是为了方便防止报错,结果到现在反而成了一种bug😭。

let禁止重复声明,而var不存在这种xian'z限制。

image

const

他必须经过初始化才行

const a;     // Uncaught SyntaxError: Missing initializer in const declaration;

临时死区(Temporal Dead Zone)

非常有意思

console.log(a);
let a = '123';    // reference 引用错误。

对比

console.log(typeof a);
if(true) {
  let a = 1;
}

image

image

因此,a的值如果处于ley的块级作用域,并且在打印后才赋值,那么是引用错误,但是如果放到块级作用域外面,它是默认从window下面拿值的,因此let之前的块级作用域称之为 Temporal Dead Zone.换句话说,块级作用域内。

第二章 模板字符串

众所周知,``代表es6的模板字符串,默认支持换行,但据说,他真正厉害的是模板标签

function passthru (literals, ...substitutions) {
  let result = '';
  // 根据substitutions的数量来确定循环的执行次数
  for (let i = 0; i < substitutions.length; i++) {
    console.log(result);
    result += literals[i];
    result += substitutions[i];
  }
  result += literals[literals.length - 1];
  return result; 
}
let count = 10;
let price = 0.25;
let message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message);

第三章 函数

函数参数默认值

function func (a=1){
    console.log(a);
}
func();   // 1;

由于a默认值等于b,而b命名在a之后,

function t (a = b,b) {
  return a + b;
}
console.log(t(1,2));    // 3
console.log(t(1));    // NaN

函数参数中的默认参数的 Temporal Dead Zone

非常有趣的函数默认参数命名 上面例子,函数参数命名过程

// t(1,2)
let a = 1;
let b = 2;

// t(undefined,1);
let a = b;  // 参数为undefined时,使用默认参数。
let b = 1;  // 在临死区,b 引用不到,而且在临死区不会去默认指向window对象的属性。

一切正如第一章所说,当a引用b的时候,b在临死区,所有的绑定行为都会报错。

所有函数参数都有自己的作用域和临死区,与其他函数体的作用域各自独立,,也就是说,函数体内部参数默认值同样不能访问函数内部声明的变量。

函数的name,

同样非常有意思,函数默认有一条属性,那就是name image image 打印函数,输出的是函数体,但(函数).name输出的确实他的函数名,或者函数声明。这同样涉及到函数背后所做的事情。

function a(){};
=====输入======
a.name
====等同于======
var temp = new Function();
temp.name    // a
temp = null    // 销毁

image

var a = (){};
console.log(a.bind().name)   // "bound dosomething";
console.log((new Function()).name)   // "anonymous";

构造函数

function P(name){
  this.name = name;
}
var person = new P('peng');
var noPerson = P('peng');

console.log(person)   // "[Object object]";
console.log(noPerson)   // "undefined";

一般来说,new让函数内部this指向新的对象,并且返回这个新的对象。 js函数有两个不同的内部方法,[[Call]]和[[Constructor]],这两个方法很有意思,当new关键字被调用的时候,执行的是[[Construct]]函数,,他会创作一个通常被称为实例的新对象,然后执行函数体,将this绑定到实例上,如果不通过new关键字调用的话,,就会执行[[Call]],从而直接执行代码中的函数体,,而具有[[Constructor]]方法的函数,被统称为构造函数, 切记,不是所有函数都有[[Construct]]方法,因此不是所有的函数都可以通过new来调用,例如后面本章所说的箭头函数就没有[[Construct]]方法,

如何判断 是否被当作构造函数来调用,

function Person (name='peng'){
  if(this instanceof Person){
    console.log('我被当作构造函数来用');
    this.name = name;
  } else {
    throw new Error('错误,前面要有new')
  }
}
new Person();
Person();

image

看上面例子,首先this会被指向新的对象,如果新的对象被指向的新对象,他的原型中又Person,那么说明你有通过构造函数构造拥有Person原型的对象;如果this的 原型是Person,即this是Person的实例,那么继续执行,如果不是,就抛出错误。由于[[Construct]]方法会创建一个Person的实例 ,并将this绑定到新的实例上面,通常,通过call也可以实现绑定。

function Person (name='peng'){
  if(this instanceof Person){
    console.log('我被当作构造函数来用');
    this.name = name;
  } else {
    throw new Error('错误,前面要有new')
  }
}
var person = new Person();
Person.call((new Person), 'micheal');

上面这种方法同样将this绑定到Person上面。Person.call()时将变量person作为第一个参数传入,相当于在Person函数里面将this设为了person实例。对于函数本身,无法通过区分是否是通过new调用的还是通过Person.call()来调用的。

通过 鉴别new.target可以判断是否是通过new来调用的

function Person (name='peng'){
  if(new.target !== undefined){
    console.log('我被当作构造函数来用',new.target);
    this.name = name;
  } else {
    throw new Error('错误,前面要有new')
  }
}
var person = new Person();
Person.call(person, 'micheal');   // throw error
function PPerson(){
  Person.call(this,name);
}
var pperson = new PPerson();   // throw error  

image

块级作用域 中的函数

if(true){
  // es5报错,es6不报错,摸棱两可的属性。
  function a(){}
}

所有的函数都会发生提升,但是问题来了,一旦if语句不执行这部分呢??

a('sdf');  // a is not defined
if(true){
  function a(){
    console.log('a');
  }
  a('sdf');  // successful;
}
a('sdf');  // a is not defined

神奇的是,函数只在块级作用域内部发生提升

typeof a;  // a is not defined
// a('sdf');  // a is not defined
if(true){
  a('sdf');  // a
  function a(){
    console.log('a');
  }
  a('sdf');  // a
  typeof a;  // a is not defined
}
// a('sdf');  // a is not defined
typeof a;  // a is not defined

但是在非严格模式下,块级函数会被提升到外围函数顶部。

箭头函数 (重点)

在ECMAScript6中,箭头函数是最有趣的的新特性。与传统函数的不同主要有以下

  • 没有this。super。arguments。和new.target绑定,箭头函数中的this,super,arguments以及new.target这些值由外围最近一层非箭头函数决定。
  • 不能通过new关键字调用, 箭头函数没有[[Construct]]方法,所以不能被当作构造函数,如果通过new调用,会报错
  • 没有原型。由于不可以通过new调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototpye这个属性,
  • 不可以改变this的绑定,在函数内部,this在生命周期内保持一致,
  • 不支持arguments对象,箭头函数没有arguments绑定,所以你必须通过命名参数和不定参数这两种形式来访问参数。
  • 不支持重复的命名参数,无论严格模式还是非严格模式,箭头函数都不支持重复命名参数。

以上差异产生原因。内部this指向不明确,因而出现箭头函数。

箭头函数同样拥有name属性, 箭头函数的IIFE版本(立即执行函数)需要包一层小括号,而正常函数包不包小括号都可以执行。

let person = (function(name){
  return {
    getName: function(){
      return name;
    }
  };
})('name');

console.log(person.getName());    // "Nicholas";

箭头函数没有this绑定

箭头函数内的this绑定是JavaScript中最常出现错误的因素,函数体内的this值可以根据函数调用上下文而改变。

let PageHandler = {
  id: '123456',
  init: function (){
    document.addEventListener('click',function (e){
      console.log(this);  // documet 对象
    },false)
  },
  doSomething: function (type){
    console.log(`handling ${type} for`,this.id)     // handling undefined for 123456
  }
}
PageHandler.init();
PageHandler.doSomething();

上面代码来看,对象PageHandler涉及初衷就是用来处理页面上面的交互,通过init来配置交互,然而实际上普通函数,this指向谁引用的它。this绑定的是目标对象的引用,上面代码中init被调用的引用是document,自然无法执行下去,想要通过bind绑定this的值,是不可能的,箭头函数this指向其外部非箭头函数。

通常在过去,通过bind方法可以显示绑定到对象中,修正这个问题。

let PageHandler = {
  id: '123456',
  init: function (){
    console.log(this);     // 指向对象本身
    document.addEventListener('click',function (e){
      console.log(this);  // PageHandler 对象
    }.bind(this) , false)
  },
  doSomething: function (type){
    console.log(`handling ${type} for`,this.id)     // handling undefined for 123456
  }
}
PageHandler.init();
PageHandler.doSomething();     // 

但是仔细一看,上面的做法很奇怪,为什么呢?(function(){}).bind(this)创建了一个新的函数。他的this指向当前对象。为了避免创建额外的函数,下面使用箭头函数。

let PageHandler = {
  id: '123456',
  init: function (){
    console.log(this);     // 指向对象本身
    document.addEventListener('click', (e)=>{
      console.log(this);  // PageHandler 对象
    } , false)
  },
  doSomething: function (type){
    console.log(`handling ${type} for`,this.id)     // handling undefined for 123456
  }
}
PageHandler.init();
PageHandler.doSomething();     // 

这个时候,this已经成功指向PageHandling本身了,再改一下,外部函数改成箭头函数

let PageHandler = {
  id: '123456',
  init:  ()=>{
    console.log(this);     // 指向对象本身
    document.addEventListener('click', (e)=>{
      console.log(this);  // window 对象  ,严格模式指向undefined
    } , false)
  },
  doSomething: function (type){
    console.log(`handling ${type} for`,this.id)     // handling undefined for 123456
  }
}
PageHandler.init();
PageHandler.doSomething();     // 

是不是很神奇,箭头函数永远指向其外部最近的普通的function的this。如果外部没有普通function,那么this指向window,或者undefined。箭头函数缺少基本的prototype,也就是说箭头函数没有原型链,箭头函数的原则就是即用即弃。, 箭头函数不能使用new来构造,因为他没有[[Construct]]方法,同时也是因为如此,JavaScript引擎可以进一步优化其特定行为。 同时,箭头函数不能通过call(),apply(),bind(),来改变this值。

image

箭头函数和数组

不多解释,非常强大,

箭头函数没有arguments绑定。这样,无论写在哪,都能通过this访问父函数的arguments。

尾递归优化

同样也是灰常强大的功能, 为什么这么说呢,这可以有效防止栈溢出。

function tip(){
  return newFunc();      // 尾调用
}

在es5引擎中,尾调用的栈同样清晰,创建一个新的栈帧(stack frame),将未完成的函数调用推入栈。但是尾递归的问题就是,当调用栈太多的时候会造成程序性能问题, 在es6引擎中,尾调用被优化,换句话说,es6引擎较少了调用栈的最大长度,如果超出长度,调用栈会先停止,转而去处理栈帧。如果满足以下三个条件,可以被js引擎自动优化调用栈。

(尼马,这一段阮一峰说的不清不楚的。)
  • 尾调用不访问当前栈的变量。(换句话说,该函数不是一个闭包)
  • 在函数尾部,尾调用是最后一句。
  • 尾调用的函数作为返回值。

如何优化一个函数

事实上尾递归优化发生在引擎后面,递归函数优化最明显,

'use strict';
function t (n, p = 1){
  if(n <=1){
    return 1*p;
  }else{
    let result = n + p;

    // 优化后
    return t(n - 1, result);
  }
}
t(12135)

然而在浏览器中打印,依然是zhan'yi'chu栈溢出。目前尚处于审查阶段。,日后再安利一波。 image

第四章 扩展对象的新功能

几乎每一种类型的值都是对象,随着js的发展,对象使用率越来越高,因此提升对象使用效率就变得非常重要。 es6也对对象进行了优化。通过许多方式加强对象的使用,通过简单的语法扩展,提供更多操作对象交互的方法,本章详细讲解这些改进。

对象类别

es6对对象进行了分类,以下4个类别

  • 普通类别(Ordinary) 具有JavaScript对象所有默认的内部行为。
  • 特异对象(Exotic) 具有某些默认行为不符的内部行为。
  • 标准对象(Standard) es6规范中定义的对象,例如 Array,Data等,标准对象可以是普通对象,也可以是特意对象。
  • 内建对象 脚本开始执行时候就存在与JavaScript内部的对象,所有标准对象都是内建对象。

下面,我们将用这些属于来解释es6定义的各种对象。

对象新增方法

通过Object.is(),我们可以检测两个值是否全等。

console.log(-0 === +0)  // true
Object.is(-0,+0);    //false
Object.is(NaN,NaN);    //true

除了NaN 或者-0这种情况,其他情况Object.is()和===基本一样。

####Object.assign()方法

今天终于见到了Mixin这个单词,原来这就是混合,真鸡毛坑啊,中文翻译真的好坑。

混合(Mixin)是JavaScript中实现函数混合对象组合的最流行的一种方式,

function minxin(receiver, superlier){
  Object.keys(superlier).forEach(function (key){
    receiver[key] = superlier[key];
  });
  return receiver;
}

上面函数会将两个对象混合在一起。因此es6出现了Object.assign这种方法。它可以改变第一个参数那个对象的属性并混入第二个对象的属性。

var t = {}
Object.assign(t,{
  a:1
},{
  a:2,
  b:3
},{
  c:5,
  d:7,
})
console.log(t);    // {a: 2, b: 3, c: 5, d: 7}

Object.assign可以接受任意个数的参数,并且越靠后的权重越高,同时,由于对象是指针关系,为了避免改变第一个参数,造成混乱,我们可以将第一个参数传入{},这样Object.assign()返回的新对象,将不会影响任何对象。注意,get属性不能被复制。

image

var o = {
  get foo(){
    return 17;
  }
}
var b = {
  a:2
}
Object.assign(b,o)
var d = Object.getOwnPropertyDescriptor(b, 'foo');  // undefined
console.log(d.get);

Object.getOwnPropertyDescriptor

非常有意思的属性,它可以获取对象的某个属性全部的值, image

简化原型访问的Super引用

正如之前说的,原型对于js非常重要,es许多改进,最终是为了让他更好用,es6引入了super,使用它可以更便捷的访问对象原型。举个例子,

let person = {
  getGreeting(){
    return 'Hello!';
  }
};

let dog = {
  getGreeting(){
    return 'Woof!';
  }
}
let friend = {
  getGreeting(){
    return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
  }
}
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting());
console.log(Object.getPrototypeOf(friend) === person);

Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting());
console.log(Object.getPrototypeOf(friend) === dog);

super则相当于对象的原型指针,上面例子即使super = Object.getPrototypeOf(this); 当然他必须在简写方式中使用,负责会抛出语法错误。 image

第五章 解构:使数据解构访问更加方便

没啥好说的。

第六章 Symbol 和 Symbol属性

私有属性,外界无法访问。所有的原始值,除了symbol以外,都有各自的字面形式,例如布尔值类型的ture或者类型值42,可以通过Symbol来创建全局一个Symbol,

let fir = Symbol();
let person = {}
person[fir] = 'Nicholas';
console.log(person[fir])    // nicholas

第七章 set和map集合

map 和 set的区别

首先安利一波mdn用法如下:第二个参数是可选参数,this, image

下面这个对象,forEach的this,作为第二个参数传入,输出结果 1,2

let set = new Set([1,2]);
let processor = {
  output(val) {
    console.log(val);
  },
  process(dataSet) {
    dataSet.forEach(function(val) {
      return this.output(val)
    }, this)
  }
}
processor.process(set);

下面这个例子,箭头函数从外围的process()函数读取this,其实直接把this当作变量来看待,他就是一个默认存在不需要声明就默认存在的变量,箭头函数内部没有this,所以你在箭头函数里面拿this会直接拿到外部的this。这个解释可以啊,

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

let processor = {
  output(val) {
    console.log(val);
  },
  process(dataSet) {
    dataSet.forEach(val => this.output(val))
  }
}
processor.process(set);

set和真数组array之间互相转换

new set(arr); // set
[...set(arr)]   // arr

谁都知道,对象是引用类型,那么当你new Set(arr)的时候,set被添加arr数组,当arr=null的时候,引用对象呗销毁,而new Set()这个值依然保留。换句话说,这是强行引用,

weakSet 弱引用

什么是弱引用,就是原来引用的对象被删除了,那么weakSet对象会同步到删除操作,然并卵。。 image image

Map 集合

Map类型是一种储存许多键值对的有序列表,其中键名和对应的值支持所有类型

Weak Map 集合

Weak Set 是若引用Set集合,相对的,Weak Map 是弱引用的Map集合,也是用于储存对象的弱引用,weakMap集合的键名必须是一个对象,如果使用非对象键名回报错,,如果在弱引用之外,不存在其他强引用,,引擎的辣鸡回收机制,会自动回收这个对象,同时移除weak Map集合种的键值对。 weak map是一种储存许多键值对的无序列表,列表的键名必须是非null的对象,键名对应的值应该是可以是任意类型,weak map 接口与map非常相似,,通过set方法设置,通过get方法得到。

let map = new WeakMap();
let element = document.querySelector(".element");
map.set(element,'Original');
let value = map.get(element);
console.log(value);                // "Original"

第七章小结

set 集合是一种包含多个非重复性的无序列表,值与值之间的等价性是通过Object.is()的方法来判断,,若果相同,就过滤,5和’5‘不同,同时,set不是数组的子类,所以你不能通过随机访问集合中的值,只能通过has()方法,检测指定的值是否存在于Set集合中,或者通过size属性查看数量, weak set集合是一个特殊的set集合,只支持存放弱引用,当其对象的其他强引用被清除的时候,弱引用自然会被清除, map是多个无序键值对组成的集合,键名支持任意数据类型,与set集合类似,map也是通过Object.is()判断是否重复,他与set的区别是可以通过迭代器循环, weak map是弱引用,造轮子应该非常适用这种东西。

我已经深刻相信,再自学下去也搞不出个什么鬼了。还是看书吧。系统性学习真的很重要。