ES6

ES6基础

Posted by Qz on August 2, 2019

“Yeah It’s on. ”

正文

rest参数

网页链接

Rest操作符和Spread操作都是用三个点(…)表示,但作用整好相反。

Rest操作符一般用在函数参数的声明中,而Spread用在函数的调用中。

'use strict';  
  
function func(...args){  
    console.log(args);//[1,2,3,4]  
}  
  
func(1,2,3,4);  

我们知道在strict mode下,对arguments做了很多限制,而且arguments是个arrayLike对象,不能像操作数组那样直接操作它。但用Rest操作符以后,args参数就是一个数组了,任何操作数组的方法都可以直接对args使用。

Object.is

网页链接

在这之前我们比较值使用两等号 == 或 三等号===, 三等号更加严格,只要比较两方类型不同立即返回false。

另外,有且只有一个值不和自己相等,它是NaN

NaN == NaN //false

现在ES6又加了一个Object.is,让比较运算的江湖更加混乱。多数情况下Object.is等价于“===”,如下

1 === 1 // true
Object.is(1, 1) // true
 
'a' === 'a' // true
Object.is('a', 'a') // true
 
true === true // true
Object.is(true, true) // true
 
null === null // true
Object.is(null, null) // true
 
undefined === undefined // true
Object.is(undefined, undefined) // true

但对于NaN、0、+0、 -0,则和 “===” 不同

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

其他

在js中我们一般认为正负0是不相等的,但是 -0 == +0 会返回true,那么怎么判断正负0 代码如下

(判断-0)
function(num){
return num == 0 && 1 / num < 0 
}
(判断+0)
function(num){
return num == 0 && 1 / num > 0 
}

Decorator

http://es6.ruanyifeng.com/#docs/decorator

https://juejin.im/entry/5b74e0525188256143054663

装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。

例子:

@frozen class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}

  @throttle(500)
  expensiveMethod() {}
}

类的装饰

@eat
class Person {
  constructor() {}
}

function eat(target, key, descriptor) {
  console.log('吃饭');
  console.log(target);
  console.log(key);
  console.log(descriptor);
  target.prototype.act = '我要吃饭';
}

const jack = new Person();
console.log(jack.act);

或者return一个函数,函数里面return一个classDescriptor

let classFactory = (name, fn) => {
  return function (classDescriptor) {
    debugger
    console.log("classDescriptor", classDescriptor)
    let {elements} = classDescriptor;

    return {
      ...classDescriptor,
       elements: elements.concat([methodDescriptorGenerator(name, fn)]),
    };
  };
};

const Wxapi = classFactory('$wxapi', ()=>{
  console.log("aaaa")
});



@Wxapi
class Person {
  constructor() {
  }
  aaa(){}
  bbb(){}
}

我们可以看到打印出来classDescriptor是

{
      kind: 'class',
      elements:
       [ Object [Descriptor] {
           kind: 'method',
           key: 'aaa',
           placement: 'prototype',
           descriptor: [Object] },
         Object [Descriptor] {
           kind: 'method',
           key: 'bbb',
           placement: 'prototype',
           descriptor: [Object] }
       ] 
}

这样子我们可以对elements进行补充,也就是增加类方法

 return {
      ...classDescriptor,
       elements: elements.concat([methodDescriptorGenerator(name, fn)]),
    };

我们去看methodDescriptorGenerator方法

let methodDescriptorGenerator = (name, fn, placement = 'prototype') => {
    return {
        key: name,
        kind: 'method',
        placement,
        descriptor: descriptorGenerator({value: fn}),
    };
};

return的对象刚好和elements中的一项数据结构对齐

其中descriptor是一个描述对象

我们继续看descriptorGenerator方法

const descriptorGenerator = (des) => {
    return {
        enumerable: true,
        writable: true,
        configurable: true,
        ...des,
    };
};

这里最重要的就是des传入的对象要有value,这里value是一个函数


上面写的比较复杂,我们来看一个简单的例子

function addFly(canFly){
  return function(target){
    target.canFly = canFly;
    let extra = canFly ? '(技能加成:飞行能力)' : '';
    let method = target.prototype.toString;
    target.prototype.toString = (...args)=>{
      return method.apply(target.prototype,args) + extra;
    }
    return target;
  }
}

@addFly(true)
class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

通过target.prototype.xxxx就可以在类上面增加方法,非常简单。

方法的装饰

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

const math = new Math();

// passed parameters should get logged now
math.add(2, 4);

或者return一个函数,函数里面return一个descriptor

/**
 * 延时执行
 * @param wait
 */
function Delay(wait = 500) {
  return function (target, name, descriptor) {
    let fn = descriptor.value;
    descriptor.value = delay(fn, wait);
    return descriptor;
  };
}

一个非常重要的点就是:this的绑定

func.apply(this, args)

/**
 * 延迟执行
 * @param func
 * @param delay
 * @returns {Function}
 */
function delay(func, delay = 500) {
  return function (...args) {
    setTimeout(() => {
      func.apply(this, args)
    }, delay)
  }
}

深入Decorator

Decorators 的本质是利用了 ES5 的 Object.defineProperty 属性,这三个参数其实是和 Object.defineProperty 参数一致的,因此不能更改

可以看看 bable 转换后 的代码,其中有一句是 descriptor = decorator(target, key, descriptor)   descriptor;

箭头函数

隐式返回值

要返回多行语句(例如对象文本),需要使用( )而不是{ }来包裹函数体。这样可以确保代码以单个语句的形式进行求值。

function calcCircumference(diameter) {
  return Math.PI * diameter
}
// 简写为:
calcCircumference = diameter => (
  Math.PI * diameter;
)

注意点

  1. 函数体内的this对象就是定义时所在的对象,而不是使用时所在对象(这点很重要)
  2. 不可以当作构造函数使用,也就是不能用new命令实例化一个对象,否则会抛出一个错误;
  3. 不可以使用arguments对象,该对象在函数体内不存在,如果要用的话,可以用rest参数代替;
  4. 不可以使用yield命令,箭头函数不能用作Generator函数;

例子

箭头函数:

var x=11;
var obj={
 x:22,
 say:()=>{
   console.log(this.x);
 }
}

obj.say();
//输出的值为11

所谓的定义时候绑定,就是this是继承自父执行上下文!!中的this,比如这里的箭头函数中的this.x,箭头函数本身与say平级以key:value的形式,也就是箭头函数本身所在的对象为obj,而obj的父执行上下文就是window,因此这里的this.x实际上表示的是window.x,因此输出的是11。(this只有在函数被调用,或者通过构造函数new Object()的形式才会有this)

var a=11;
function test2(){
  this.a=22;
  let b=()=>{console.log(this.a)}
  b();
}
var x=new test2();
//输出22

很奇怪对不对,我是这样理解的,ES6中定义的时候绑定this的具体含义,应该继承的是父执行上下文里面的this

注意:简单对象(非函数)是没有执行上下文的!

箭头函数中,this指向的固定化,并不是因为箭头函数内部有绑定this的 机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外 层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

我们可以来模拟ES5中的箭头函数转化:

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

非箭头函数

var a=11
function test1(){
  this.a=22;
  let b=function(){
    console.log(this.a);
  };
  b();
}
var x=new test1();
// 输出11
var fullname = 'John Doe';
var obj = {
   fullname: 'Colin Ihrig',
   prop: {
      fullname: 'Aurelio De Rosa',
      getFullname: function() {
         return this.fullname;
      }
   }
};
 
console.log(obj.prop.getFullname());
// Aurelio De Rosa
 
var test = obj.prop.getFullname;
 
console.log(test());
// John Doe

JavaScript中关键字this所引用的是函数上下文,取决于函数是如何调用的,而不是怎么被定义的。

在第一个console.log(),getFullname()是作为obj.prop对象的函数被调用。因此,当前的上下文指代后者,并且函数返回这个对象的fullname属性。相反,当getFullname()被赋值给test变量时,当前的上下文是全局对象window,这是因为test被隐式地作为全局对象的属性


彻底弄懂箭头函数

class A {
  	constructor() {
		this.b = this.b.bind(this);    	
    }
  
    a() {
    	console.log('a');
    }
	  b() {
    	console.log('b')
    }
    c = () => {
    	console.log('c')
    }
}

babel编译后

"use strict";

function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }

function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

// 向Constructor.prototype中添加东西
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var A = /*#__PURE__*/function () {
  function A() {
    _classCallCheck(this, A);

    // 箭头函数编译后  
    _defineProperty(this, "c", function () {
      console.log('c');
    });

    this.b = this.b.bind(this);
  }

  _createClass(A, [{
    key: "a",
    value: function a() {
      console.log('a');
    }
  }, {
    key: "b",
    value: function b() {
      console.log('b');
    }
  }]);

  return A;
}();


  • 普通函数: 在 babel 编译后,会被放在函数的 prototype 上

  • constructor 里 bind 的函数: 在编译后,它不仅会被放在函数的 prototype 里,而且每一次实例化,都会产生一个绑定当前实例上下文的变量(this.b = this.b.bind(this))。

  • 箭头函数:在 babel 编译后,每一次实例化的时候,都会调用 defineProperty 将箭头函数内容绑定在当前实例上下文上。

从编译后的结果来看的话,对于实际开发的时候,如果需要绑定上下文的话,最好还是用箭头函数。因为使用 bind 方式的话,不仅会产生一个 prototype 的函数,每一次实例化都会额外产生多一个函数。

引申出class知识点

class 对于 = 号声明的方法、变量,都会将其作为实例的属性,而对于非 = 号声明的属性,则是放在原型链上

class A {
    a() {
        
    }
    b = 2;
    c = () => {
    }
}

对于这个类, 在实例化的时候,b, c 会作为实例的属性,而 a 则是放在原型链上。

举个例子:

class Yideng {
  static str = "京程一灯"
  // 赋值 会将其作为实例的属性
  sayStr = () => {
    throw new Error("Need to implement")
  }
}


class Student extends Yideng {
  constructor(){
    super()
  }

  // 声明的属性 放在原型链上
  sayStr(){
    console.log(Student.str)
  }
}


const laoyuan = new Student()
console.log(Student.str) // 京程一灯

// 先找实例的属性再去原型链找 (存在实例属性) 
laoyuan.sayStr()   // Error("Need to implement")

class

es6 的类在初始化的时候,如果有基类会先执行基类的构造函数,之后再执行本身的构造函数。

this 的指向

观察最近写的框架插件,思考了一下js元解析器解析插件中链式调用的函数会存在重复调用的问题。

最终,发现可能是this指向的问题

更改前

class BucketControl {
  static getInstance(bucketConfig) {
    if (!BucketControl.instance) {
      BucketControl.instance = new BucketControl(bucketConfig);
    }
    return BucketControl.instance;
  }

  constructor(bucketConfig = {}) {
    let buckets = {};
    Object.keys(bucketConfig).forEach(key => {
      buckets[key] = new Bucket({name: key, ...bucketConfig[key]});
    });
    this.buckets = buckets;
    this.init();
  }
  
  .... 
}



module.exports = function (config) {
  // 这里之所以要设置成类 是因为bucketControl初始化要依赖一个config
  let bucketControl = BucketControl.getInstance(config);
  return {
    name: 'bucketControl',
    customMethod: {
      add: bucketControl.add.bind(bucketControl),
      addRead: bucketControl.addRead.bind(bucketControl),
      remove: bucketControl.remove.bind(bucketControl),
      choose: bucketControl.choose.bind(bucketControl),
      print: bucketControl.print.bind(bucketControl),
      delAll: bucketControl.delAll.bind(bucketControl),
      delRead: bucketControl.delRead.bind(bucketControl),
    },
  };
};

更改后:


class BucketControl {
  static getInstance(bucketConfig) {
    if (!BucketControl.instance) {
      BucketControl.instance = new BucketControl(bucketConfig);
    }
    return BucketControl.instance;
  }

  constructor(bucketConfig = {}) {
    let buckets = {};
    Object.keys(bucketConfig).forEach(key => {
      buckets[key] = new Bucket({name: key, ...bucketConfig[key]});
    });
    this.buckets = buckets;
    // this 的指向 (防止函数被单独拿出来使用)
    // http://es6.ruanyifeng.com/#docs/class
    this.add = this.add.bind(this);
    this.addRead = this.addRead.bind(this);
    this.remove = this.remove.bind(this);
    this.choose = this.choose.bind(this);
    this.print = this.print.bind(this);
    this.delAll = this.delAll.bind(this);
    this.delRead = this.delRead.bind(this);
    this.init();
  }
  
  ...
}  
  
module.exports = function (config) {
  // 这里之所以要设置成类 是因为bucketControl初始化要依赖一个config
  let bucketControl = BucketControl.getInstance(config);
  return {
    name: 'bucketControl',
    customMethod: {
      add: bucketControl.add,
      addRead: bucketControl.addRead,
      remove: bucketControl.remove,
      choose: bucketControl.choose,
      print: bucketControl.print,
      delAll: bucketControl.delAll,
      delRead: bucketControl.delRead,
    },
  };
};

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错。

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}

类方法不可枚举

最近在仿写axios,在写Axios类的时候发现类方法不可枚举

例子

  class Axios {
    constructor() {
      this.ccc = "ccc"
    }

    aaa(aaa) {
      console.log(aaa)
    }

    bbb(bbb) {
      console.log(bbb)
    }

    request() {
      console.log("request")
    }
  }

  let aaa = new Axios()

  for (let item in aaa) {
    console.log("item", item)
  }
  
  // 输出 "ccc"

按理说class只是一个语法糖,aaa,bbb这些存在于Axios.prototype之中,使用for…in是可以打印出来的,但是由于类方法不可枚举,所以这里只能打印出”ccc”

这一点与 ES5 的行为不一致。 这一点与 ES5 的行为不一致。 这一点与 ES5 的行为不一致。


如果我真的很想打印出”aaa”这些,该怎么办?

方法一:

改成function的形式

  function Axios() {

  }

  Axios.prototype.aaa = () => {
    console.log("aaa")
  }

方法二:

将类方法bind到类实例的this上

  class Axios {
    constructor() {
      this.ccc = "ccc"
      this.aaa = this.aaa.bind(this)
    }

    aaa(aaa) {
      console.log(aaa)
    }
  }

static

在class中

  • static属性会定义在实例的 constructor上 (也就是类转义得到的函数上)
  • 普通属性会定义在实例上

来看一个例子:

class Yideng {
   str = "京城一等"
   static aaa="aaa"
}

const obj = new Yideng()
console.log(obj.aaa) // undefined
console.log(obj.constructor.aaa) //aaa
console.log(Yideng.aaa) //aaa
console.log(obj.constructor == Yideng) // true
console.log(obj.__proto__ == Yideng.prototype) // true 

经过babel转义后

"use strict";

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var Yideng = function Yideng() {
  _classCallCheck(this, Yideng);
  // 定义在实例上	
  _defineProperty(this, "str", "京城一等");
};

// 定义在this.constructor上
_defineProperty(Yideng, "aaa", "aaa");

计算属性

网页链接

let propKey = 'foo';  
let obj = {//ES6  
    [propKey]: true,  
    ['b'+'ar']: 123  
};  
  
//ES6之前只能这样写:  
obj[propKey + "1"] = false;  
obj['f'+'oo2'] = 456;  
  
console.log(obj);//Object { foo: true, bar: 123 }  

ES6之前要想用变量当作对象的属性名称,必须先声明一个字面量,然后在用中括号语法赋值,ES6中直接可以在声明的时候就使用中括号语法来使用变量。

对象的方法一样可以使用这样写法:

et obj = {  
    ['h'+'ello']() {  
        return 'hi';  
     }  
};  
console.log(obj.hello()); // hi  

在Vuex中的mutations里

const mutations = {
  [types.SET_SINGER](state, singer) {
    state.singer = singer
  },
  [types.SET_PLAYING_STATE](state, flag) {
    state.playing = flag
  },
  [types.SET_FULL_SCREEN](state, flag) {
    state.fullScreen = flag
  }
}

Proxy

https://es6.ruanyifeng.com/#docs/proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

var obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  }
});

Proxy 缺点

this 问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

Proxy 拦截函数内部的this,指向的是handler对象。

Reflect

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

vue3 proxy劫持数据的幕后支持者之一。

反射的原理是通过编译阶段对对象注入元数据信息,在运行阶段读取注入的元数据,从而得到对象信息。

Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

元数据反射(Reflect Metadata) 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。

要在 ts 中启用元数据反射相关功能需要:

  • npm i reflect-metadata --save
  • tsconfig.json 里配置 emitDecoratorMetadata 选项为true

内置元数据

TypeScript 结合自身语言的特点,为使用了装饰器的代码声明注入了 3 组元数据:

  • design:type:成员类型
  • design:paramtypes:成员所有参数类型
  • design:returntype:成员返回类型

reflect-metadata

https://www.npmjs.com/package/reflect-metadata

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据,可以被用于类,类成员以及参数。

reflect-metadata是一种增强描述,我们可以在不侵入原类、原函数或者原属性的前提下,对其进行额外的描述,例如这样:

class A {
  b: {}
}

Reflect.defineMetadata('test', '1', A)
Reflect.defineMetadata('testb', '2', A, 'b')

console.log(Reflect.getMetadata('test', A)) // '1'
console.log(Reflect.getMetadata('testb', A, 'b')) // '2'

Symbol

symbol 英文意思为 符号、象征、标记、记号,在 js 中更确切的翻译应该为 独一无二的值,理解了它的意思之后再看起来就简单多了。

可以通过下面的方式来创建一个 symbol 类型的值

const s = Symbol();

console.log(typeof s);  // "symbol"

需要注意的是通过 Symbol 方法创建值的时候不用使用 new 操作符,原因是通过 new 实例化的结果是一个 object 对象,而不是原始类型的 symbol

Symbol 方法接收一个参数,表示对生成的 symbol 值的一种描述

const s = Symbol('foo');

即使是传入相同的参数,生成的 symbol 值也是不相等的,因为 Symbol 本来就是独一无二的意思

const foo = Symbol('foo');
const bar = Symbol('foo');

console.log(foo === bar); // false

Symbol.for 方法可以检测上下文中是否已经存在使用该方法且相同参数创建的 symbol 值,如果存在则返回已经存在的值,如果不存在则新建。

const s1 = Symbol.for('foo');
const s2 = Symbol.for('foo');

console.log(s1 === s2); // true

Symbol.keyFor 方法返回一个使用 Symbol.for 方法创建的 symbol 值的 key

const foo = Symbol.for("foo");
const key = Symbol.keyFor(foo);

console.log(key) // "foo"

使用场景

消除魔法字符

假如现有一个 Tabs 切换的功能

if (type === 'basic') {
    return <div>basic tab</div>
}

if (type === 'super') {
    return <div>super tab</div>
}
复制代码

上面代码中字符串 basic、super 就是与业务代码无关的魔法字符,接下来使用 Symbol 对这块代码进行改造

const tabTypes = {
    basic: Symbol(),
    super: Symbol(),
}

if (type === tabTypes.basic) {
    return <div>basic tab</div>
}

if (type === tabTypes.super) {
    return <div>super tab</div>
}

作为对象属性

当一个复杂对象中含有多个属性的时候,很容易将某个属性名覆盖掉,利用 Symbol 值作为属性名可以很好的避免这一现象。

const name = Symbol('name');
const obj = {
    [name]: 'ClickPaas',
}

Symbol.iterator部署遍历器生成方法

https://es6.ruanyifeng.com/#docs/iterator

对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value); // 0, 1, 2
}

上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。

下面是通过遍历器实现指针结构的例子。

function Obj(value) {
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
  var iterator = { next: next };

  var current = this;

  function next() {
    if (current) {
      var value = current.value;
      current = current.next;
      return { done: false, value: value };
    } else {
      return { done: true };
    }
  }
  return iterator;
}

var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);

one.next = two;
two.next = three;

for (var i of one){
  console.log(i); // 1, 2, 3
}

下面是另一个类似数组的对象调用数组的Symbol.iterator方法的例子。

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

手写symbol

https://github.com/mqyqingfeng/Blog/issues/87

function SymbolPolyfill(description) {
  // 不能使用new
  if (this instanceof SymbolPolyfill) {
    throw new TypeError('Symbol is not a constructor')
  }

  const descStr = description == undefined ? undefined : String(description)

  const symbol = Object.create(null)

  Object.defineProperties(symbol, {
    __Description__: {
      value: descStr,
      writable: false,
      enumerable: false,
      configurable: false,
    },
  })

  // 返回的是一个新对象,两个对象之间,只要引用不同,就不会相同
  return symbol
}

Array

find和findIndex

网页链接

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

[1, 4, -5, 10].find((n) => n < 0)  
// -5  
[1, 5, 10, 15].find(function(value, index, arr) {  
return value > 9;  
}) // 10  

上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

[1, 5, 10, 15].findIndex(function(value, index, arr) {  
return value > 9;  
}) // 2  

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。 另外,这两个方法都可以发现NaN,弥补了数组的IndexOf方法的不足。

[NaN].indexOf(NaN)  
// -1  
[NaN].findIndex(y => Object.is(NaN, y))  
// 0  

上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

Array.from

JavaScript 中有很多克隆数组的方法。正如你所想,Array.from() 可以很容易的实现数组的浅拷贝。

const numbersCopy = Array.from(numbers);

numbers === numbersCopy; // => false

Array.from(numbers) 创建了对 numbers 数组的浅拷贝,numbers === numbersCopy 的结果是 false,意味着虽然 numbersnumbersCopy 有着相同的项,但是它们是不同的数组对象。

for in

遍历对象 通常用for in来遍历对象的键名

for in 可以遍历到myObject的原型方法method,如果不想遍历原型方法和属性的话,可以在循环内部判断一下,hasOwnPropery方法可以判断某属性是否是该对象的实例属性

for (var key in myObject) {
  ifmyObject.hasOwnProperty(key)){
    console.log(key);
  }
}

可以通过ES5的Object.keys(myObject)获取对象的实例属性组成的数组,不包括原型方法和属性

for..of和for..in可以正确响应break、continue和return语句

forEach

无法return和break

我们都知道for循环里要跳出整个循环是使用break,但在数组中用forEach循环如要退出整个循环使用break会报错,使用return也不能跳出循环。

之所以不能break, return

是因为它不是简单的for循环。内部实现类似callback(context, arg1, arg2)。

Array.prototype.forEach = function (callback) {
  // this represents our array
  for (let index = 0; index < this.length; index++) {
    // We call the callback for each entry
    callback(this[index], index, this);
  }
}

async/await 与 forEach 也没法结合使用

map

const result = [{ aa: "aa" }, { bb: "bb" }]
const res = result.map(item => item)
res[0].aa = "test"
console.log(res)  // 改变
console.log(result)  // 同时改变

证明map循环中是浅拷贝

正确用法:

const result = [{ aa: "aa" }, { bb: "bb" }]
const res = result.map(item => ({
  ...item
}))
res[0].aa = "test"
console.log(res) // 改变
console.log(result) // 不会改变

forEach和map

map方法体现的是数据不可变的思想。该思想认为所有的数据都是不能改变的,只能通过生成新的数据来达到修改的目的,因此直接对数组元素或对象属性进行操作的行为都是不可取的。这种思想其实有很多好处,最直接的就是避免了数据的隐式修改。immutable.js是实现数据不可变的一个库,可通过专属的API对引用类型进行操作,每次形成一个新的对象。

forEach和map还存在一个编程思想的区别,前者是命令式编程,后者是声明式编程,如果项目的风格是声明式的,比如React,那么后者显然更统一。

共同点:

  1. 都是循环遍历数组中的每一项。
  2. forEach()和map()里面每一次执行匿名函数都支持3个参数:数组中的当前项item,当前项的索引index,原始数组input。
  3. 匿名函数中的this都是指Window。
  4. 只能遍历数组。

try catch能捕获到哪些 JS 异常

能捕捉到的异常, 必须是在报错的时候 ,线程执行已经进入 try catch 但 try catch 未执行完的时候抛出来的。

异步方法无法捕捉到


try{    
    setTimeout(()=>{      
        console.log(a.b); 
    },100)}catch(e){    
  
    console.log('error',e);
    }
	console.log(111);
//output111Uncaught ReferenceError: a is not defined

setTimeout 里面报错,实际上是 100ms 之后执行的代码报错,此时代码块 try catch 已经执行完成,111 都已经被执行了,故无法捕捉异常。


一个特殊的例子:

 try {
    new Promise(function (resolve, reject) {
      a.b;
    }).then((v) => {
      console.log(v);
    });
  } catch (e) {
    console.log("error", e);
  } 
  // outputUncaught (in promise) ReferenceError: a is not defined

看如上报错,线程在执行 a.b 的时候,事实上属于同步执行,try catch 并未执行完成,按理应该能捕捉到异常,这里为啥无法捕捉呢?

事实上,Promise 的异常都是由 reject 和 Promise.prototype.catch 来捕获,不管是同步还是异步。

核心原因是因为 Promise 在执行回调中都用 try catch 包裹起来了,其中所有的异常都被内部捕获到了,并未往上抛异常。

不要用 try catch 包裹 Promise , Promise 很强大,不用担心异常会往上抛!我们只需要给 Promise 增加 Promise.prototype.catch 就 OK 了

Generator

Generator 是 ES6 推出的一种新的数据类型,它本质上是 JS 协程的一种实现

什么是协程

https://www.liaoxuefeng.com/wiki/897692888725344/923057403198272

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

img

举个例子:

注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:

def A():
    print '1'
    print '2'
    print '3'

def B():
    print 'x'
    print 'y'
    print 'z'

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:

1
2
x
y
3
z

但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。

看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行

  • 最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
  • 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

补充

CORS blocked when type=“module”

https://forum.freecodecamp.org/t/cors-blocked-when-i-use-type-module/340229

<script  type="module" src="../toy-react/day2/toy-react.js"></script>

当我们使用 type=”module”引入一个es6模块的本地js,会发生跨域问题

解决:

开一个本地服务器。