浅记 - JS变量

132 阅读7分钟

ECMAScript 的变量是松散类型,可以用于保存任何类型的数据

3 个关键字用于声明变量:varconstlet

var 可以在所有版本中使用,constlet 只能在 ES6 及更晚的版本中使用

命名

方法

匈牙利命名法

变量名 = 属性 + 类型 + 对象描述

  • Int         整数型 i
  • Float        浮点型 fl
  • Boolean       布尔 b
  • String         字符串 s
  • Array         数组 a
  • Object         对象 o
  • Function         函数 fn
  • Regular Expression  正则 re

驼峰命名法

1. 全部小写

  • 单词与单词间用下划线分割

    var get_node_child;

2. 大小写混合

  • 大驼峰

    每个单词首字母大写

    var GetNodeChild;

  • 小驼峰

    第一个单词首字母小写,后续首字母大写

    var getNodeChild;

规则

ECMAScript 中所有都区分大小写

  • 首字符

    字母、下划线 ( _ ) 或者美元符号 ( $ )

  • 组成

    字母、数字、下划线 ( _ ) 或者美元符号 ( $ )

  • 禁忌

    JavaScript 中的关键字、保留字、true、false 和 null 都不能作为标识符


声明

关键字

格式:关键字 变量名 (不初始化的情况下,变量会保存特殊值 undefined)

let name;

console.log(name); // 返回undefined

var

  • 声明提升( hoist )

    使用 var 声明的变量会自动提升到函数作用域的顶部,但是赋值不会提升

function fun() {
  console.log(age);
  var age = 25;
}
fun(); // undefined

let

  • 暂时性死区( temporal dead zone )

    let 声明的变量不会在作用域中被提升

console.log(name); // undefined
var name = "John";

console.log(newName); // ReferenceError: newName 没有定义
let newName = "John";

在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”( temporal dead zone ),在此 阶段引用任何后面才声明的变量都会抛出 ReferenceError

const

const 行为与 let 基本相同,需要注意的是 const 声明变量的同时必须初始化变量

  1. 不允许重复声明

  2. 作用域范围是块

  3. 不能修改常量的值

  4. 不能用 const 来声明迭代变量(因为迭代变量会自增)

    for(const i = 0; i < 10; i++) {} // 报错

不过可以用 const 声明一个不会修改的 for 循环变量

let i = 0;
for(const j = 7; i < 5; i++) {
  console.log(j);
}
// 7, 7, 7, 7, 7

批量定义

在一条语句中用逗号分隔每个变量及可选的初始化

let message = "world",
    found = false,
    age = 25;

其中插入空格和换行不是必需的,其目的只是为了利于阅读

流程

  • 先声明,后读写
  • 先赋值,后运算

不同点

letconst在全局声明的变量不会保存在顶层对象( window <global> )里面,而var1.主要的区别在于lexicalEnvironment用于存储函数声明和变量( letconst )绑定,而ObjectEnvironment仅用于存储变量( var )绑定
2.由于letconst这类的词法环境都属于Declarative Environment Records和函数、类这些一样,在单独的存储空间,var这类,属于Object Environment Record,会挂载到某个对象上,也会沿着原型链去向上查找
var u = 23;
console.log(window.u === u); // true
let o = 9;
console.log(window.o); // undefined

变量类型

原始值 ( primitive value )

  1. 占用固定空间,保存在栈中

  2. 不能有属性,尽管不报错 (只有引用值可以动态添加使用的属性)

    let name = "Nicholas";

    name.age = 27;

    console.log(name.age); // undefined

  3. 使用 typeof 检测数据的类型

  4. 保存与复制的是值本身

    • 复制是把值作为副本传递给其他变量,副本被其他变量改变后,也不会影响到自身的值

    • 在内存中通过变量把一个原始值赋值到另一个变量是,原始值会被复制到新变量得位置,两个变量可以独立使用,互不干扰

      let num1 = 5;

      let num2 = num1;

引用类型 ( reference value )

  1. 占用空间不固定,保存在堆内存中

  2. 使用 instanceof 检测数据的类型

    通过 instanceof 操作符检测任何引用值和 Object 构造函数都会返回 true;但如果用 instanceof 检测原始值,则始终返回 false

  3. 使用 new() 方法构造出的对象是引用型

  4. 保存与复制是指向对象的一个指针

    • 值会保存在全局作用域的堆内存上,堆内存里的值被任一对象改变了,其他对象再指向它时,会返回最后一次被修改成的值
    • 把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置,相当于是把这个值在堆内存中的地址告诉了另外一个变量,让另外一个变量也能通过这个地址访问和使用到这个值。因为有两个变量都指向同一个对象,所以就符合了第一条的逻辑
let obj1 = new Object();
let onj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // Nicholas

误区:当在局部作用域中修改对象而变化反映到全局时,就意味着参数就是按引用传递的

function setName(obj) {
  obj.name = "John";
  obj = new Object();
  obj.name = "Greg";
  console.log(obj.name); // Greg
}

let person = new Object();
setName(person);
console.log(person.name); // John

当 person 传入 setName() 时,其 name 属性被设置为"John"。然后变量 obj 被设置为一个新对象且 name 属性被设置为"Greg"。如果 person 是按引用传递的,那么 person 应该自动将指针改为指向 name 为"Greg"的对象;结果并非如此,这表示函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针,而本地对象在函数执行结束时就被销毁


作用域

全局变量

可以在任何位置调用到全局变量

  • 在函数体外定义的变量可作用至全局
  • 在函数内部省略关键字去声明一个变量,该变量会变成全局变量

虽然可以通过省略关键字操作符定义全局变量,但不推荐这么做,会导致后期代码难以维护

省略var关键字

function demo() {
  message = 'hello';  // 全局变量
}
demo();
console.log(message); // hello

let 在全局作用域中声明的变量不会成为 window 对象的属性,var 声明的变量则会;不过 let 声明仍然在全局作用域中发生

var a = 2;
alert(window.a); // 2

let b = 3;
alert(window.b); // undefined

局部变量

在当前块作用域内调用的变量以及函数的参数变量

var

- **var 声明范围是函数作用域**

使用 var 在函数内部定义一个变量,形成函数作用域中的局部变量;当函数在退出时被销毁。
```
function demo() {
    var message = 'hello'; // 局部变量
}
demo();
console.log(message);  // undefined
```
- **var 会忽略块级作用域**

```
for(var i = 0; i < 5; i++) {

}
console.log(i); // 5
```

let

- **let 声明范围是块作用域 { }** *(比 var 的声明范围更加严苛)*
let 声明的变量无法在 if 块外部被引用;因为块作用域是函数作用域的子集,所以 var 的作用域限制同样会限制 let
```
if(true) {
  var name = "Matt";
  console.log(name); // Matt
}
console.log(name); // Matt

if(true) {
  let newName = 'John';
  console.log(newName); // John
}
console.log(newName); // ReferenceError: newName 没有定义
```

- **let 在 for 循环定义的迭代变量不会渗透至循环体外部**

因为迭代变量作用域仅限于 for 循环块内部,而且 JS 后台会为每次迭代循环声明一个新的迭代变量

```
for (var i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0)
}
// 输出 5、5、5、5、5(因为在退出循环时,迭代变量保存的是导致循环退出的值:5)

for (let i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0)
}
// 会输出 0、1、2、3、4
```


- **同一作用域不能出现冗余声明,否则会报错;此外,声明冗余报错并不会因为混用 let 和 var 而受影响**

同一作用域下声明变量

```
let age;
let age; // SyntaxError; 表示标识符age已经声明过了
```
不同作用域声明变量

```
let age = 30;
console.log(age); // 30
if(true) {
  let age = 26;
  console.log(age); // 26
}
```
var、let 混用
```
let num;
var num; // SyntaxError;

var name;
let name; // SyntaxError;
```

优先级

  • 局部变量高于同名全局变量
  • 参数变量高于同名全局变量
  • 局部变量高于同名参数变量

全局变量对象始终时作用域链的最后一个变量对象

特性

1. 作用域链

  • 内层函数可访问外层函数的局部变量
  • 外层函数不能访问内层函数的局部变量

2. 生命周期

  • 全局变量

    除非被显示删除,否则会一直存在

  • 局部变量

    自声明起至函数运行完毕或被显示删除

  • 回收机制

    • 标记清除 ( mark-and-sweep )
    • 引用计数 ( reference counting )

如文中有描述不对的地方,还请各位同仁斧削