Appearance
js 变量声明整体可以分为六种:
let x
-- 声明变量 x。不可在赋值之前读。const x
-- 声明常量 x。不可在赋值之前读,不可写。var x
-- 声明变量 x。在赋值之前可读取到 undefined 值。function x
-- 声明变量 x。该变量指向一个函数。class x
-- 声明变量 x。该变量指向一个类(该类的作用域内部是处理严格模式的)。import
-- 导入标识符并作为常量(可以有多种声明标识符的模式和方法)
上面的六种大体可以归类为三种情况 var
,const
,let
- 属于
var
归类:var
和function
声明变量,这两种声明方式都会发生变量提升,即在声明之前使用变量会得到 undefined 值,而不会报错。它们的作用域是函数作用域。 - 属于
const
归类:const
和import
声明变量, 这两种声明方式都是常量,不能被重新赋值。在声明前使用会报错。它们的作用域是块级作用域。正是如此也可以更好的解释import
声明为什么要放到最上面,import
声明的变量是常量,不能被重新赋值,且在声明前就使用会报错。和const
声明的变量一样,import
声明的变量也是块级作用域。 - 属于
let
和class
声明变量,这两种声明方式都不能在声明前使用,否则会报错。它们的作用域是块级作用域。特别地,class
声明的内部是处于严格模式中的。
要注意一个强调的变量声明,声明和语句是编程中两个不同的概念
声明是用来告诉编译器某个变量的类型和名称,以便在编译时分配内存空间。声明发生在编译期,编译器会为所声明的变量在相应的变量表中增加一个名字。
语句是用来执行某些操作的代码,例如赋值、条件判断、循环等。声明和语句的区别在于它们发生的时间点不同。语句是在运行期执行的程序代码,需要CPU来执行。如果声明不带初始化,编译器可以完全处理,不会产生运行时执行的代码,因为编译器已经知道了变量的类型和名称,可以在编译时分配内存空间。
JavaScript 将可以通过 静态 语法分析发现那些声明的标识符;在变量声明阶段,JavaScript引擎会为变量分配内存空间,并将变量绑定到该内存空间。
对于使用var关键字声明的变量,它们的初始值会被设置为undefined;对于使用let和const关键字声明的变量,它们在声明阶段不会被赋值。
因此标识符对应的变量 / 常量(let var 声明的就是变量可以重新赋值,const 属于常量) 一定 会在用户代码执行前就已经被创建在作用域中(也就是声明阶段)。
js 变量声明赋值过程
举个例子 var myArray= []
赋值过程经历步骤
变量声明:在编译阶段(声明),JavaScript 引擎会为变量
myArray
进行声明。此时,变量尚未赋值,其值为undefined
。变量赋值:在执行阶段(语句),JavaScript 引擎会创建一个空数组对象
[]
。这个对象会被存储在堆内存中,同时分配一个内存地址。将内存地址赋值给变量:将存储空数组对象的内存地址赋值给变量
myArray
。这样,myArray
就指向了空数组对象。动态调整:由于 JavaScript 是一种动态语言,数组的大小和内容可以在运行时动态调整。当向数组中添加或删除元素时,内存中的空间会相应地进行调整。
对比来看静态语言声明赋值过程
在静态语言(如 C++、Java 等)中,变量的类型和初始值在编译阶段就已经确定。这与动态语言(如 JavaScript)有所不同,动态语言在运行时才会为变量分配内存和赋值。int[] myArray = []
这个语句在静态语言(如 Java)中为例
变量声明与类型:int[] myArray 声明了一个整型数组变量 myArray。这意味着 myArray 只能存储整数类型的元素。
变量赋值:int[] myArray = [] 语句在 编译阶段(声明时候) 为 myArray 分配内存并初始化为空数组。这意味着 myArray 的长度为 0,没有任何元素。
内存管理:在静态语言中,数组的大小通常是固定的。如果需要改变数组的大小,需要创建一个新的数组并将原数组的元素复制到新数组中。在 Java 中,可以使用 System.arraycopy() 方法实现这一功能。
类型安全:由于静态语言在编译阶段就确定了变量的类型,因此具有更强的类型安全。在本例中,myArray 只能存储整数类型的元素,如果尝试向其中添加其他类型的元素,编译器会报错。
静态语言在编译阶段就确定了变量的类型和初始值,这使得程序在执行过程中具有更高的确定性和性能。然而,这也意味着静态语言的灵活性相对较低,因为在运行时不能轻易地改变变量的类型和内存空间。
var x = y = 100 发生什么
结合上面,要知道 js 两个阶段 声明 和 语句运行 , var/let/const
声明 和 语句运行的赋值阶段不同
js
var a = {} // 这是 var 声明
a.b = 10 // 这是语句运行的赋值
var/let/const
声明语句中的等号左边,绝不可能是一个表达式。声明阶段一定是发生在编译期
因此,根本上来说,在 var 声明 语法中,变量名位置上就是写不成a.x的
js
var a.x = ... // <- 这里将导致语法出错
在看这个案例 var x
在 声明阶段也就是编译期间就已经存在了
但 此时 y 变量并没有被声明定义 (在当前作用域(scope)中没有被声明使用var、let或const关键字定义的变量),将赋值给未声明变量(let/const/var)的值在执行赋值时将其隐式地创建为全局变量 这种变量也叫做隐式全局变
声明变量在任何代码执行前创建,而非声明变量只有在执行赋值操作的时候才会被创建。通过下面代码可以证明 var x 是在声明阶段 ,而 y 是在运行阶段
js
// 非声明 变量隐式全局变量如果没有上来赋值就报错
console.log(a); // 抛出ReferenceError。
console.log('still going...'); // 永不执行。
// var 这中声明变量就不会有这个问题
var a;
console.log(a); // 打印"undefined"或""(不同浏览器实现不同)。
console.log('still going...'); // 打印"still going..."。
- 也通过
getOwnPropertyDescriptor
来验证configurable
如果是true 怎说明是一个隐式全局变量 为false 则是一个声明
js
var x = y = 100
console.log(Object.getOwnPropertyDescriptor(window,'y'))
console.log(Object.getOwnPropertyDescriptor(window,'x'))
console.log(x,y)
打印结果:
{value: 100, writable: true, enumerable: true, configurable: true}
{value: 100, writable: true, enumerable: true, configurable: false}
100 100
因此整个过程可以拆解为,因此并不推荐这种连续赋值
js
var x; // 变量提升声明 x
y = 100 // 隐式全局变量在特定情况下会出现泄漏
x = y // 变量赋值
console.log(x,y)
打印结果:
100 100
这种隐士变量的好处代码中不需要显式地声明一个变量了,变量可以随用随声明,也不用像后来的let语句一样,还要考虑在声明语句之前能不能访问的问题了。相对的也会出现一个问题 变量泄漏,在少量的代码中也相当易用。但是,如果代码规模扩大,变成百千万行代码,可能因为忘记本地的声明而读写了全局变量,不方便调试
还是要特别强调要注意 ,声明和赋值的区别他们完全是在两个阶段,var/let/const
声明语句中的等号左边,绝不可能是一个表达式 , var/let/const 关键字只负责声明变量名,而不是进行赋值操作,他的过程属于 初始器(Initializer),初始器是用来在声明变量时给变量赋予初始值的。在初始器中,"=" 号不是一个运算符,而是一个语法分隔符,用于将变量名与其初始值关联起来。所以,虽然初始器看起来像是一个赋值操作,但实际上它并不是一个严格意义上的赋值操作。
Initializer: = AssignmentExpression
在 "var x = 100" 这个语句中,"var x" 是创建变量名的部分,而 "= 100" 是给变量 x 赋初始值的部分。x 只是一个表达名字的、静态语法分析期作为标识符来理解的字面文本,而不是一个表达式。
- 用 "var"、"let" 或 "const" 关键字创建变量时,这些关键字的作用是告诉 JavaScript 我们要创建一个变量,而不是给变量赋值。
- 当我们创建变量的同时给它赋一个初始值,这个过程叫做 "初始器"。在这个过程中,"=" 号的作用是把初始值和变量关联起来,而不是进行数学运算。
因此ECMAScript 6 之后的模板赋值的左侧也不是表达式,只要是声明语句中,那么就 不是
js
const [a,b] = [2,3] // 左侧不是表单式
也可以说 并没有连续声明,var x 是声明 y 是赋值,关于赋值可以参考下面
赋值
看完了变量声明在来看赋值操作, js 的赋值操作也并不是单纯的 变量名 = 值
整个赋值的过程在规范中来看是 LeftHandSideExpression = AssignmentExpression
,也就是将右操作数(的值)赋给左操作数(的引用),操作数既可以是值也可以是字面量。因此 JavaScript 中,一个赋值表达式的左边和右边其实都是表达式! 可以 参考 规范ECMAScript Language Specification sec-assignment-operators
赋值是语句执行时候过程,因此左侧可以作为表达式
LeftHandSideExpression 和 AssignmentExpression
具体来看规范中对 LeftHandSideExpression 和 AssignmentExpression 的解释
LeftHandSideExpression(左表达式) ,在规范中对
LeftHandSideExpression
可以是以下三种类型之一NewExpression :包含
MemberExpression[?Yield, ?Await]
的定义比较多可以参考 但是举一个例子 a.b 这种对象属性形式 和new NewExpression[?Yield, ?Await]
使用 new 关键字创建一个对象实例。例如,new MyClass()CallExpression:这是一个用于调用函数的表达式。?Yield和?Await表示在这个表达式中是否允许使用yield和await关键字。
OptionalExpression :这是一个可选链表达式,用于访问对象属性或调用方法,如果对象或属性不存在,则返回undefined。?Yield和?Await表示在这个表达式中是否允许使用yield和await关键字
AssignmentExpression[In, Yield, Await]: 赋值表达式是将一个值分配给一个左手边表达式的表达式 在规范里面定义
- ConditionalExpression[?In, ?Yield, ?Await][+Yield]
- YieldExpression[?In, ?Await]
- ArrowFunction[?In, ?Yield, ?Await]
- AsyncArrowFunction[?In, ?Yield, ?Await]
- LeftHandSideExpression[?Yield, ?Await]=AssignmentExpression[?In, ?Yield, ?Await]
- LeftHandSideExpression[?Yield, ?Await]AssignmentOperatorAssignmentExpression[?In, ?Yield, ?Await]
- LeftHandSideExpression[?Yield, ?Await]&&=AssignmentExpression[?In, ?Yield, ?Await]
- LeftHandSideExpression[?Yield, ?Await]||=AssignmentExpression[?In, ?Yield, ?Await]
- LeftHandSideExpression[?Yield, ?Await]??=AssignmentExpression[?In, ?Yield, ?Await]
AssignmentOperator:one of *= /= %= += -= <<= >>= >>>= &= ^= |= **=
对上述规则的解释:
ConditionalExpression[?In, ?Yield, ?Await]
:条件表达式,通常用于三元操作符(condition ? expression1 : expression2
),根据条件选择执行哪个表达式。
js
// 三元表达案例
let age = 18;
let isAdult = (age >= 18) ? '成年' : '未成年';
[+Yield] YieldExpression[?In, ?Await]
:Yield表达式,用于生成器函数中暂停和恢复执行。
js
function* generator() {
yield 1;
yield 2;
yield 3;
}
const iter = generator();
console.log(iter.next()); // 输出:{ value: 1, done: false }
ArrowFunction[?In, ?Yield, ?Await]
:箭头函数,一种简洁的函数表达式,使用=>
符号定义。
js
const sum = (a, b) => a + b;
console.log(sum(3, 4)); // 输出:7
AsyncArrowFunction[?In, ?Yield, ?Await]
:异步箭头函数,一种支持异步操作的箭头函数。
js
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
};
fetchData();
LeftHandSideExpression[?Yield, ?Await] = AssignmentExpression[?In, ?Yield, ?Await]
:基本赋值操作,使用=
符号将右侧表达式的值赋给左侧变量。
js
let a;
a = 10;
console.log(a); // 输出:10
LeftHandSideExpression[?Yield, ?Await] AssignmentOperator AssignmentExpression[?In, ?Yield, ?Await]
:复合赋值操作,如*= /= %= += -= <<= >>= >>>= &= ^= |= **=
等,将左侧变量与右侧表达式的值进行操作后重新赋值给左侧变量。
js
let b = 5;
b += 3;
console.log(b); // 输出:8
LeftHandSideExpression[?Yield, ?Await] &&= AssignmentExpression[?In, ?Yield, ?Await]
:逻辑与赋值操作,仅当左侧变量为真时,将右侧表达式的值赋给左侧变量。
js
let c = true;
c &&= false;
console.log(c); // 输出:false
LeftHandSideExpression[?Yield, ?Await] ||= AssignmentExpression[?In, ?Yield, ?Await]
:逻辑或赋值操作,仅当左侧变量为假时,将右侧表达式的值赋给左侧变量。
js
let d = 0;
d ||= 10;
console.log(d); // 输出:10
LeftHandSideExpression[?Yield, ?Await] ??= AssignmentExpression[?In, ?Yield, ?Await]
:空值合并赋值操作,仅当左侧变量为null
或undefined
时,将右侧表达式的值赋给左侧变量。
js
let e = null;
e ??= 'default';
console.log(e); // 输出:'default'
对LeftHandSideExpression = AssignmentExpression 详细说明
LeftHandSideExpression[?Yield, ?Await] = AssignmentExpression[?In, ?Yield, ?Await]
AssignmentExpression 是一个递归操作 注意 上面说到的 AssignmentExpression 表达的含义有九种,因此 所分配解答最开始 解释的 js 并不是简单的 值赋值操作
这里对 ?In 做个说明这个参数表示表达式是否在in操作符的上下文中。in操作符用于检查对象是否具有某个属性。例如:
js
let obj = { key: "value" };
let result = "key" in obj; // result为true,因为obj具有"key"属性
补充一个知识点
在上面的左侧表达是里,知道左侧其实可以是一个 new 对象 但实际 new Object = 1;
赋值报错( Invalid left-hand side in assignment)参考1,参考2
- CallExpression :包含函数调用。例如,myFunction() 或者 myFunction.a 更多定义。
- OptionalExpression :使用可选链操作符(?.)检查对象属性或方法的存在。例如,obj?.prop。
最主要的还是要看规范中的介绍
赋值表达式:LeftHandSideExpression = AssignmentExpression
如果左边的表达式(LeftHandSideExpression)既不是一个对象字面量(ObjectLiteral)也不是一个数组字面量(ArrayLiteral),如果赋值操作符左边的表达式既不是对象字面量也不是数组字面量,那么它可能是一个变量名、函数调用、成员访问等其他类型的表达式那么:
a. 计算左边的表达式并将结果存储在lref中(首先计算赋值操作符左边的表达式的值,并将这个值存储在一个叫做lref的地方)。
b. 如果赋值表达式(AssignmentExpression)是一个匿名函数定义,并且左边的表达式是一个标识符引用,即赋值操作符右边的表达式是一个匿名函数,并且左边的表达式是一个**变量名**。在这种情况下,JavaScript会使用左边的变量名作为匿名函数的名字那么: i. 使用lref.[[ReferencedName]]作为参数,对赋值表达式进行命名评估,并将结果存储在rval中。 c. 否则: i. 计算赋值表达式并将结果存储在rref中。 ii. 获取rref的值并将结果存储在rval中。 d. 将rval的值赋给lref。 e. 返回rval的值。
让assignmentPattern成为由左边的表达式所覆盖的赋值模式。
计算赋值表达式并将结果存储在rref中。
获取rref的值并将结果存储在rval中。
使用rval作为参数,对assignmentPattern进行解构赋值评估。
返回rval的值。
ObjectLiteral (对象字面量)也不是 ArrayLiteral(数组字面量),这里和 我们的情况还是不一样 new Object = 1;
中 new Object
并不是字面量,他所拦截的错误案例在
js
// 这是一个不正确的示例,因为我们试图将值赋给一个对象字面量
{ key: "value1" } = { key: "value2" };
报错信息 Uncaught SyntaxError: Unexpected token '='
也和 我们的报错信息 Invalid left-hand side in assignment
在苹果电脑执行Left side of assignment is not a reference.
也不同实际报错位置发生在 PutValue(lref,rval)。在PutValue操作的情况下,如果V不是引用记录,容易导致对不存在的属性或值进行赋值或访问。因此,在这个步骤中,会抛出一个ReferenceError异常,表示这是一个错误的引用,程序无法正常执行。简单地说,这是一种防止程序员犯错的保护措施,确保代码的正确性。
a.x = a ={n:2}
js
var a = {}
a.x = a ={n:2}
console.log(a.n) // 2
console.log(a.x) // undefined
简化赋值规范再来看这个问题
Let lref be the result of evaluating LeftHandSideExpression.
Let rref be the result of evaluating AssignmentExpression.
Let rval be ? GetValue(rref).
Perform ? PutValue(lref, rval).
Return rval.
这个代码是从左到右解析的,但赋值过程是从右到左,下面打印的结果 Evaluating the left16 side / Evaluating the left6 side /Evaluating the right2 side
虽然做了运算正常来说算除法 但实际并不是 是先从左到右解析 然后在根据规范计算先除法在加法
js
function echo(name, num) {
console.log("Evaluating the " + name+num + " side");
return num;
}
// 注意这里的除法运算符 (/)
console.log(echo("left", 16)+echo("left", 6) / echo("right", 2));
打印结果
因此按照规范来看 LeftHandSideExpression = AssignmentExpression 注意之前说过 AssignmentExpression 是一个递归表达,因此a.x = a ={n:2}
应该拆解为 a.x = (a = {n:2})
也就是 LeftHandSideExpression =( LeftHandSideExpression = AssignmentExpression)
按照规范来说第一步计算左边的表达式并将结果存储在lref中(首先计算赋值操作符左边的表达式的值,并将这个值存储在一个叫做lref的地方),此时被保存起来的 a.x 指向的是 还有进行操作的 我们这里叫 最初a
值是空对象,再来看 后面a = {n:2}
这里的 a 虽然也是 最初a
值是空对象,但因为被赋值的原因此时 a 已经变成了 新a
{n:2}
,现在就可以拆解为 a.x = {n:2}
注意此时 a.x a是最初a
,也就是 PutValue({}, {n:2})
在最初的 a上赋值了 对新的没有影响
所以,当你执行 console.log(a.n) 时,输出的是 2,因为这是新对象的n属性的值。而当你执行 console.log(a.x) 时,输出的是 undefined,因为在新对象上并没有x属性,x属性是在原来的对象上,但是原来的对象已经被新的对象替代了,所以无法访问到。
- 图从左到右看,绿色代表
最初a
红色表示新a
通过步骤还原
js
var a = {}
a.x = a ={n:2}
// 此时 a.x 中的a 指向 var a ={} 并且存在了一个空间中
// a = {n:2} 的 a 原本也是 var a = {} 但将 此时 a 进行了重新赋值 var a 变成了 a ={n:2}
// 注意此时 a.x 虽然指向是 var a = {} 并且 新的 var a ={n:2}但是 在左边表达式时候 原 var a ={} 已经保存了
// a.x => {} => {} = {n:2},现在 在原始的 a 上 a.x = {n:2} 但新 a 上还是 {n:2} 没有 x 属性
// 下面打印的是谁 实际是新 a
console.log(a.n) // 2
console.log(a.x) // undefined
- 证明上面说的,我们用ref 保存我们说的最初 a,执行代码后
js
// ref 保存的是上一次的a
var a = {n:1}, ref = a;
a.x = a = {n:2};
console.log(a.x); // --> undefined
console.log(ref.x); // {n:2}
- 另一种证明方法,第二次赋值操作中,将尝试向“原始的变量a”添加一个属性“a.x“,且如果它没有冻结的话,属性
a.x
会指向第一次赋值的结果
js
// 声明“原始的变量a”
var a = {n:1};
// 使它的属性表冻结(不能再添加属性)
Object.freeze(a);
try {
// 本节的示例代码
a.x = a = {n:2};
}
catch (x) {
// 异常发生,说明第二次赋值“a.x = ...”中操作的`a`正是原始的变量a
console.log('第二次赋值导致异常.');
}
// 第一次赋值是成功的
console.log(a.n); //
换个角度通过运算符优先级角度来看
其中 赋值=
优先级别是 2 ,成员访问.
是18,运算符优先级越大优先级越高。想了解更多的同学可以查看 MDN。
由于访问成员.的优先级大于赋值的优先级,所以赋值时这样的:
js
a.x = a = {n: 2}
转为:
a.x = {n: 2};
a = {n: 2};
参考
一道阿里面试题引起的思考:var foo = {n: 1}; var bar = foo; foo.x = foo = {n: 2};