4-ES 新特性-1(01-02-01)

144 阅读6分钟

ES6

1、ECMAScript和JavaScript的关系

  1. ECMAScript是由网景的布兰登·艾奇开发的一种脚本语言的标准化规范,是规范
  2. JavaScript 是一种基于 ECMAScript 规范的脚本语言,是实现

2、ES var和let

  1. let声明的成员只会在所声明的块中生效
  2. var会变量升级
  3. let 应用场景:循环绑定事件,事件处理函数中获取正确索引
  4. for 循环会产生两层作用域
  5. let 修复了变量声明提升现象

3、for setTimeout 经典题目

for (var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
     }, 1000 );
}

解析:浏览器会输出5个6。

这是因为setTimeout是异步执行,每一次for循环的时候,setTimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里,等待执行。只有主线上的任务执行完,才会执行任务队列里的任务。也就是说它会等到for循环全部运行完毕后,才会执行fun函数,但是当for循环结束后此时i的值已经变成了6,因此虽然定时器跑了5秒,控制台上的内容依然是6。

  1. 解决方案:闭包
for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
            console.log( j );
        }, j*1000 );
    })(i);
}

我们可以发现跟预期结果一致,依次输出1到5,因是因为实际参数跟定时器内部的i有强依赖。 通过闭包,将i的变量驻留在内存中,当输出j时,引用的是外部函数的变量值i,i的值是根据循环来的,执行setTimeout时已经确定了里面的的输出了。 2. 解决方案 拆分结构

function timer(i) {
    setTimeout( console.log( i ), i*1000 );
}
for (var i=1; i<=5;i++) {
    timer(i);
}
  1. 解决方案:let
for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
     }, 1000 );
}

这个例子与第一个相比,只是把var更改成了let,可是控制台的结果却是依次输出1到5。

  因为for循环头部的let不仅将i绑定到for循环中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值重新被赋值。setTimeout里面的function()属于一个新的域,通过var定义的变量是无法传入到这个函数执行域中的,通过使用let来声明块变量能作用于这个块,所以function就能使用i这个变量了;这个匿名函数的参数作用域和for参数的作用域不一样,是利用了这一点来完成的。这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。

4、const

  1. 在let上增加了只读特性
  2. 恒量声明过后不允许重新赋值
  3. 恒量只是要求内层指向不允许被修改,对于数据成员的修改是没有问题的
const obj = {}
obj.name = 'zce' // 可以
obj = {} //不可以

5、(...)展开运算符\剩余运算符

合并数组

let a = [1,2,3];
let b = [4,5,6];
let c = [...a,...b]; // [1,2,3,4,5,6]

数据构造 两个对象连接返回新的对象

let x = {
	name: 'autumn'
}
let y = {
	age: 18
}
let z = {...x,...y}
console.log(z)

解构赋值

let a = [1,2,3,4,5,6]
let [c,...d] = a
console.log(c); // 1
console.log(d); // [2,3,4,5,6]
//展开运算符必须放在最后一位

浅拷贝

//数组
var a = [1,2,4]
var b = [...a]
a.push(6)
console.log(b) // [1,2,4]

//对象
var a = {a:1}
var b = {...a}
a.a = 5
console.log(b.a) // 1

函数传参

 // ES5 的写法
 function f(x, y, z) {
 // ...
 }
 var args = [0, 1, 2];
 f.apply(null, args);
 // ES6 的写法
 function f(x, y, z) {
 // ...
 }
 var args = [0, 1, 2];
 f(...args);

剩余参数

// function foo () {
//   console.log(arguments)
// }

function foo (first, ...args) {
  console.log(args)
}

foo(1, 2, 3, 4)

6、模板字符串

// 可以通过 ${} 插入表达式,表达式的执行结果将会输出到对应位置
const msg = `hey, ${name} --- ${1 + 2} ---- ${Math.random()}`
console.log(msg)
  • 带标签的模板字符串
  • 模板字符串的标签就是一个特殊的函数,
  • 使用这个标签就是调用这个函数
const name = 'tom'
const gender = false

function myTagFunc (strings, name, gender) {
  // console.log(strings, name, gender)
  return strings[0] + name + strings[1] + gender + strings[2]
}

const result = myTagFunc`hey, ${name} is a ${gender}.`

console.log(result)
// hey,tom is a false.

7.字符串的扩展方法

const message = 'Error: foo is not defined.'

console.log(
  message.startsWith('Error') // true 是不是Error开头
  message.endsWith('.')  // true 是不是.结尾
  message.includes('foo')  // true 是不是包含foo
)

8.参数默认值

//   以前的写法
function foo (enable) {
//   // 短路运算很多情况下是不适合判断默认参数的,例如 0 '' false null
//   // enable = enable || true (错误的写法
     enable = enable === undefined ? true : enable
     console.log(enable)
}

//   现在的写法
function foo (enable = true) {
     console.log(enable)
}
foo(false)
foo()

9、箭头函数

  1. 写法不一样
  2. 普通函数存在变量提升的现象
  3. 箭头函数不能作为构造函数使用
  4. 两者this的指向不同
  5. 箭头函数的arguments指向它的父级函数所在作用域的arguments
  6. 箭头函数没有new.target
function inc (number) {
 return number + 1
}

// 最简方式
const inc = n => n + 1

// 完整参数列表,函数体多条语句,返回值仍需 return
const inc = (n, m) => {
console.log('inc invoked')
return n + 1
}
console.log(inc(100))

// 常用场景,回调函数
const arr = [1, 2, 3, 4, 5, 6, 7]
arr.filter(function (item) {
 return item % 2
})

arr.filter(i => i % 2)

10、箭头函数与this

  • 普通函数的this指向的是谁调用该函数就指向谁

  • 如果没有调用者,那这个this就指向window

  • 箭头函数不会改变 this 指向

  • 箭头函数的this指向的是在你书写代码时候的上下文环境对象的this,如果没有上下文环境对象,那么就指向最外层对象window。

// xixi是全局函数,普通函数没有人调用this的时候,指向window
function xixi() {
    console.log(this)
}
xixi(this)

>>Window

//  第一个this指向obj,而setTimeout里面的匿名函数没有直接调用者,this指向window
let obj = {
    num: 10,
    xx: function() {
        console.log(this);
        setTimeout(function() {
            console.log(this);
        })
    }
}
obj.xixi()

>>Object
>>Window

//  如果我把setTimeout里面的普通函数,改成箭头函数,里面的this就指向它外层的this,就是obj
let obj = {
    num: 10,
    xx: function() {
        console.log(this);
        setTimeout(() =>{
            console.log(this);
        })
    }
}
obj.xixi()

>>Object
>>Object

11、对象字面量的增强

  • 属性名与变量名相同,可以省略 : bar
  • 方法可以省略 : function,这种方法就是普通的函数,同样影响 this 指向。
  • 通过 [] 让表达式的结果作为属性名
  method1: function () {
    console.log('method111')
   }
  // 方法可以省略 : function
  method1 () {
    console.log('method111')
    // 这种方法就是普通的函数,同样影响 this 指向。
    console.log(this)
  },
  
  obj[Math.random()] = 123 // 以前
  obj{
      [Math.random()] = 123 // 现在
  }

12、 Object.assign

  • Object.assign 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象上。
  • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
  • 如果只有一个target(目标对象),Object.assign会直接返回该对象
  • 如果该参数不是对象,则会先转成对象,然后返回
  • Object.assign方法是浅拷贝
var a = 1;
Object.assign(a);
console.log(a); // 1
console.log(typeof a); // number
console.log(typeof Object.assign(2)) // object
  • 对于null, undefined 来说 无法转换成Object,就会在控制台下报错
Object.assign(null);  // 报错
Object.assign(undefined);  // 报错
  • 对象合并,如果源对象是null的话或者是undefined的话,那么对象合并的时候不会报错,直接会跳过该对象的合并,直接返回目标对象。
var obj = {a: 1};
console.log(Object.assign(obj, null) === obj); // true
console.log(obj); // {a: 1}

var obj = {a: 1};
console.log(Object.assign(obj, undefined) === obj); // true
console.log(obj); // {a: 1}
  • 如果是数值,布尔型,和字符串合并对象的话,都不会报错,但是字符串会以数组的形式表现。
先看数值合并对象如下代码:
var obj = {a: 1};
console.log(Object.assign(obj, 12) === obj); // true
console.log(obj); // {a: 1}

布尔型合并对象如下代码:
var obj = {a: 1};
console.log(Object.assign(obj, true) === obj); // true
console.log(obj); // {a: 1}

字符串合并对象如下代码:
var obj = {a: 1};
console.log(Object.assign(obj, "bcd") === obj); // true
console.log(obj); // {0: 'b', 1: 'c', 2: 'd', a: 1}
如上代码,只有字符串和对象合并,这是因为只有字符串有包装对象,会产生可枚举类型属性。比如如下代码:
console.log(Object('bcd')); // {0: "b", 1: "c", 2: "d", length: 3, [[PrimitiveValue]]: "bcd"}
console.log(Object(1111)); // {[[PrimitiveValue]]: 1111}
console.log(Object(true)); // {[[PrimitiveValue]]: true}
const source1 = {
   a: 123,
   b: 123
}

const source2 = {
   b: 789,
   d: 789
}

const target = {
   a: 456,
   c: 456
}
const result = Object.assign(target, source1)
console.log(target) // {a: 123,c:456,b:123}
console.log(result == target) // true

13、Object.is

  • JS中==、===和Object.is()的区别
  1. == 等同,比较运算符,两边值类型不同的时候,先进行类型转换,再比较;
  2. === 恒等,严格比较运算符,不做类型转换,类型不同就是不等;
  3. Object.is()是ES6新增的用来比较两个值是否严格相等的方法,与===的行为基本一致。
  • 先说===,这个比较简单,只需要利用下面的规则来判断两个值是否恒等就行了。
  1. 如果类型不同,就不相等
  2. 如果两个都是数值,并且是同一个值,那么相等;
  3. 值得注意的是,如果两个值中至少一个是NaN,那么不相等(判断一个值是否是NaN,可以用isNaN()或Object.is()来判断)。
  4. 如果两个都是字符串,每个位置的字符都一样,那么相等;否则不相等。
  5. 如果两个值都是同样的Boolean值,那么相等。
  6. 如果两个值都引用同一个对象或函数,那么相等,即两个对象的物理地址也必须保持一致;否则不相等。
  7. 如果两个值都是null,或者都是undefined,那么相等。
  • 再说Object.is(),其行为与===基本一致,不过有两处不同: +0不等于-0。 NaN等于自身。
+0 === -0 //true
NaN === NaN // false

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

14、Object.defineProperty 的缺点

我们总结一下Object.defineProperty在劫持对象和数组时的缺陷:

  1. 无法检测到对象属性的添加或删除
  2. 无法检测数组元素的变化,需要进行数组方法的重写
  3. 无法检测数组的长度的修改

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

var user = {};

var initName = ''
Object.defineProperty(user, "name", {
    get: function(){
        console.log('get name')
        return initName
    },
    set: function(val){
        console.log('set name')
        initName = val
    }
});
// get name
console.log(user.name)
// set name
user.name = 'new'
Vue.set(object, key, value) 
this.$set(object, key, value) 

Proxy

  • 相较于Object.defineProperty劫持某个属性,Proxy则更彻底,不在局限某个属性,而是直接对整个对象进行代理,我们看一下ES6文档对Proxy的描述:
  • Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

不管是数组下标或者数组长度的变化,还是通过函数调用,Proxy都能很好的监听到变化;而且除了我们常用的get、set,Proxy更是支持13种拦截操作。

可以看到Proxy相较于Object.defineProperty在语法和功能上都有着明显的优势;而且Object.defineProperty存在的缺陷,Proxy也都很好地解决了。

Proxy的优势如下

  1. Proxy可以直接监听整个对象而非属性。
  2. Proxy可以直接监听数组的变化。
  3. Proxy有13中拦截方法,如ownKeys、deleteProperty、has 等是 Object.defineProperty 不具备的。
  4. Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;
  5. Proxy做为新标准将受到浏览器产商重点持续的性能优化,也就是传说中的新标准的性能红利。
 const person2 = {
  name: 'zce',
  age: 20
}

const personProxy = new Proxy(person2, {
  get (target, property) {
    console.log('get', property)
    return target[property]
  },
  set (target, property, value) {
    console.log('set', property, value)
    target[property] = value
  }
})

personProxy.name = 'jack'

console.log(personProxy.name)

Reflect 对象

const obj = {
  name: 'zce',
  age: 18
}

// console.log('name' in obj)
// console.log(delete obj['age'])
// console.log(Object.keys(obj))

console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))