对象字面量的增强

ES6中对 对象字面量 进行了增强,称之为 Enhanced object literals(增强对象字面量)。

字面量的增强主要包括下面几部分:

  • 属性的简写:Property Shorthand
  • 方法的简写:Method Shorthand
  • 计算属性名:Computed Property Names ```javascript var name = ‘zs’ var age = 18

// 对象字面量的方式创建对象,并且想要拥有上面的 name,age 属性 var obj = { name: name, // 在obj对象中声明name属性并赋值name变量的值zs age: age,

eat: function() { // 字面量定义方法,先定义属性名再写方法体 console.log(this.name + ‘ eating’); } }

obj[name + 123] = ‘hhh’ // 给 obj 对象添加一个动态计算的属性,只能动态添加

console.log(obj); // { name: ‘zs’, age: 18, eat: [Function: eat], zs123: ‘hhh’ }

//—- 对象字面量增强 —- var obj1 = { name, // 属性简写,直接写变量名,会自动创建同名属性并赋值 age,

eat() { // 方法简写,会自动创建方法名属性 console.log(this.name + ‘ eating’); },

[name + 123]: ‘hhh’ // 计算属性直接在定义对象时添加 }

console.log(obj1); // { name: ‘zs’, age: 18, eat: [Function: eat], zs123: ‘hhh’ }

  1. <a name="xb2Yv"></a>
  2. # 解构
  3. ES6中新增了一个从数组或对象中按需方便获取数据的方法,称之为解构 Destructuring。
  4. <a name="g7eEm"></a>
  5. ## 数组中解构
  6. 数组是靠索引取值的,所以解构也是按顺序的一个一个取。
  7. ```javascript
  8. var names = ["abc", "cba", "nba"]
  9. // 传统获取数组的值,babel 也会将解构转成这样
  10. // var item1 = names[0]
  11. // var item2 = names[1]
  12. // var item3 = names[2]
  13. // 对数组的解构: []
  14. var [item1, item2, item3] = names
  15. console.log(item1, item2, item3) // abc cba nba
  16. // 解构后面的元素
  17. var [, , itemz] = names
  18. console.log(itemz) // nba
  19. // 解构出一个元素,后面的元素放到一个新数组中
  20. var [itemx, ...newNames] = names // 类似剩余参数操作符
  21. console.log(itemx, newNames) // abc [ 'cba', 'nba' ]
  22. // 解构的默认值为 undefined
  23. var [itema, itemb, itemc, itemd] = names
  24. console.log(itemd) // undefined 数组中并没有第四个元素,解构不出来,就按默认值来
  25. // 可以给解构设置默认值
  26. var [itema, itemb, itemc, itemd = "aaa"] = names
  27. console.log(itemd) // aaa

对象中的结构

和数组不同的是对象中的属性是 key-value 的形式,所以解构取值和顺序没关系。

  1. var obj = {
  2. name: "why",
  3. age: 18,
  4. height: 1.88
  5. }
  6. // 对象的解构: {}
  7. var { name, age, height } = obj
  8. console.log(name, age, height)
  9. // 和顺序无关,所以不需要逗号占位隔开
  10. var { age } = obj
  11. console.log(age)
  12. // 将解构的值赋值给新变量,或者说是取了个别名
  13. var { name: newName } = obj
  14. console.log(newName)
  15. // 取别名并赋结构默认值
  16. var { address: newAddress = "广州市" } = obj
  17. console.log(newAddress)duix

解构的场景

  • 定义参数时的传参

当明知函数的参数是对象或者数组的时候,而我们只是想要里面的某个值,就可以在传参的时候解构取值。

  • 函数返回值

函数的返回值是数组或者对象,则可以再接收参数的时候解构取自己需要的值

  1. var arr = ['abc', 'cba', 'nba']
  2. var obj = {
  3. name: 'why',
  4. age: 18,
  5. height: 1.88
  6. }
  7. // 传统取值
  8. function foo(arr, obj) {
  9. // 希望获取数组的第二个值和对象的name属性
  10. console.log(arr[1], obj.name)
  11. }
  12. foo(arr, obj) // cba why
  13. // 参数解构取值
  14. function fn([, result], {name}) {
  15. console.log(result, name)
  16. }
  17. fn(arr, obj) // cba why
  1. var obj = {
  2. name: 'why',
  3. age: 18,
  4. height: 1.88
  5. }
  6. function foo(obj) {
  7. obj.age += 1
  8. return obj
  9. }
  10. // 传统想要处理过的对象属性
  11. var newObj = foo(obj)
  12. console.log(newObj.age); // 19
  13. // 返回值解构
  14. var {age: newAge1} = foo(obj)
  15. console.log(newAge1); // 20

let / const

基本使用

从ES6开始新增了两个关键字可以声明变量:let、const。let、const在其他编程语言中都是有的,所以也并不是新鲜的关键字。

  • let关键字:

从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量

  • const关键字:

const关键字是constant的单词的缩写,表示常量、衡量的意思;它表示保存的数据一旦被赋值,就不能被修改;但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;因为 const 保存的其实对象堆内存的地址。

注意:另外let、const不允许重复声明变量;

⭐新版本下变量在内存中的存储

之前我们知道,在 ECMA 的早期版本,执行上下文中有变量对象 VO,但是现在的版本改了,已经不存在 VO 了。
image.png
image.png
现在执行上下文中的是变量环境 VE,变量和函数声明都会作为环境记录放入 VE 中。VE 也指向一个堆内存中的对象,这个对象依然会有 VE 中的属性和函数声明。

ECMA 是个规范,只是规定需要有 VE 指向这么一个对象。那么各家 JS 引擎实现可能会不一样。比如 v8 中的这个对象为**variables_**,类型 VariableMap,通过 hashmap 来实现了它们的存储。

新版本下的 window 对象

在早期的版本中,全局定义的变量会添加到 window 对象上作属性,window 对象也能直接访问全局变量。
因为 GO 对象中的东西,window 对象也需要。所以早期 v8 引擎让 window 指向 GO,window 对象和 GO 对象其实就是同一个对象。

  1. var foo = "foo"
  2. var message = "Hello World"
  3. // 全局定义的变量也添加到了 window 对象上
  4. console.log(window.foo)
  5. console.log(window.message)
  6. // window 对象上的变量也相当于定义到了全局
  7. window.message = "哈哈哈"
  8. console.log(message)

而在最新的实现中已经变了。全局的参数和属性都会保存在variables_对象中。v8 引擎已经不实现 window 对象,而是让包含 v8 引擎的浏览器来实现。window 对象已经独立出来作为一个独立对象,里面也包含了一些 v8 引擎没实现的东西。

但现在有个疑问了,为啥 var 现在定义的全局变量还是会被添加到 window 对象中作为属性?
因为浏览器为了兼容老代码,一直保持了 window 和 var 之间值的相等性。也就是会把 var 定义的变量从 variables_ 对象中同步到 window 对象,而 let、const 定义的变量则不会。

但这显然是过渡作法,随着 var 的弃用,window 对象肯定会剔除这些实现变成一个纯净的对象。

let/const 作用域提升

我们知道var声明的变量是会进行作用域提升的;但是如果我们使用let声明的变量,在声明之前访问会报错;

  1. console.log(h) // ReferenceError: Cannot access 'h' before initialization
  2. let h = 123

变量提升是怎么来的?在词法分析时,变量被添加到了执行上下文环境中,但是还没有执行,所以默认值是 undefined。
也就是还没执行变量的时候,变量就已经被创建出来了。而且访问该变量也是没有问题的,这就造成了一种变量还未声明但是可以访问的假象。

而 let、const 它们声明的变量也会在词法分析时被添加到上下文环境中,但和 var 不同的是,虽然变量被添加到了上下文环境中,但是变量在还没执行到初始化操作之前,是不允许访问的

let、const 到底有没有作用域提升?
这个有争议,纯粹看你怎么定义作用域提升。但是有一点是肯定的,那就是 let、const 和 var 一样,声明的变量在赋值前就已经被创建出来了。
image.png

块级作用域

之前 JavaScript 只会形成两个作用域:全局作用域和函数作用域。
在 ES6 中新增了块级作用域的概念,并且对 let、const、function、class声明的类型有效。
但是尽管 ECMA 规范要求块级作用域对 function 有效,但是大部分浏览器为了兼容以前的代码, 让 function 是没有块级作用域限制的。

  1. {
  2. var n1 = 123
  3. let n2 = 456
  4. function demo() {
  5. console.log("demo function")
  6. }
  7. class Person {}
  8. }
  9. console.log(n1); // 123,var 无视块级作用域,正常访问
  10. console.log(n2); // n2 is not defined
  11. demo() // demo function 浏览器开后门
  12. new Person() // Person is not defined

if、switch、for 本来就是代码块。所以它们三个 es6 开始也会形成自己的块级作用域。

  1. // if语句的代码就是块级作用域
  2. if (true) {
  3. var foo = 'foo'
  4. let bar = 'bar'
  5. }
  6. console.log(foo) // foo
  7. console.log(bar) // bar is not defined
  8. // switch语句的代码也是块级作用域
  9. switch ('red') {
  10. case 'red':
  11. var foo = 'foo'
  12. let bar = 'bar'
  13. }
  14. console.log(foo) // foo
  15. console.log(bar) // bar is not defined
  16. // for语句的代码也是块级作用域
  17. for (var i = 0; i < 10; i++) {
  18. }
  19. console.log(i) // 10
  20. for (let j = 0; i < 10; i++) {
  21. }
  22. console.log(j) // j is not defined

块级作用域的应用场景

比如:页面有四个按钮想要点击按钮弹出提示点击了哪个,就给元素绑定点击事件。

  1. <html lang="zh_CN">
  2. <head></head>
  3. <body>
  4. <button>按钮1</button>
  5. <button>按钮2</button>
  6. <button>按钮3</button>
  7. <button>按钮4</button>
  8. </body>
  9. </html>
  1. const btns = document.getElementsByTagName('button')
  2. for (var i = 0; i < btns.length; i++) {
  3. btns[i].onclick = function() {
  4. console.log("第" + i + "个按钮被点击")
  5. }
  6. }

测试会发现,无论点击哪个按钮都会提示 点击了第四个按钮。因为点击事件会在全局代码执行完才会执行,这个时候 i 已经等于 4 了,回调函数去全局中获取 i 的值,所以获取的 i 都是 4。

没有 let 之前,我们只能借助函数作用域来包裹一下,强行套上一层作用域。并且函数需要使用立即执行函数。全局代码执行,立即执行函数就会把每次+1 的 i 当参数传进自己的函数代码块内,因为函数作用域的存在,回调函数就会先在立即执行函数中找到 i,而不会去全局找了。

  1. const btns = document.getElementsByTagName('button')
  2. for (var i = 0; i < btns.length; i++) {
  3. (function(n) {
  4. btns[i].onclick = function() {
  5. console.log("第" + n + "个按钮被点击")
  6. }
  7. })(i) // i 当参数传给立即执行函数
  8. }

显然这样是很麻烦的,而 let 识别代码块作用域就很好的解决这个问题。

  1. for (let i = 0; i < btns.length; i++) {
  2. btns[i].onclick = function() {
  3. console.log("第" + i + "个按钮被点击")
  4. }
  5. }

上面的代码能不能用 const 定义 i 呢?
首先要说明一下,循环是怎么执行的。每循环一次,其实就会出现一个代码块,然后在每个代码块中 let 声明一个 i,这个 i 会接收上一个代码块 +1 的值来初始化。const 在每个代码块中声明没问题,但是 +1 的操作却做不到,所以不行。

但是在一些循环中不需要有值的变化,则 const 是可以的,比如for...of遍历数组。

  1. for (const item of arr) {
  2. console.log(item)
  3. }

暂时性锁区

在ES6中,我们还有一个社区提出的概念称之为暂时性死区:
它表达的意思是在一个代码块中,使用 let、const声明的变量,在声明之前,变量都是不可以访问的;

  1. var foo = "foo"
  2. // 整个 if 代码块就称为 foo 的死区
  3. if (true) {
  4. // 虽然 var 全局声明了 foo,但依然无法访问
  5. console.log(foo) // ReferenceError: Cannot access 'foo' before initialization
  6. let foo = "abc"
  7. }

其实就是 let 声明的变量不能在赋值前使用的问题,报错都是一样的。
至于 var 全局声明了,还是不能访问,那是因为找属性是沿着作用域链查找。if 代码块已经形成作用域了,而作用域里已经有 foo 属性,就不会去全局找。

var、let、const 的选择

对于 var 的使用:
我们需要明白一个事实,var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题;其实是JavaScript在设计之初的一种语言缺陷。

对于let、const:
我们会优先推荐使用 const,这样可以保证数据的安全性不会被随意的篡改;
只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用 let;
这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;