|
声明变量的关键字 |
在ES6之前,声明变量的关键字只有var
,作用域只有:全局作用域和函数作用域;
到了ES6,引入let,const
两个关键字声明变量和常量,同时引入新的作用域:块级作用域。
声明变量的形式 |
ES5 只有两种声明变量的方法:var
命令和function
命令。
ES6 除了添加let
和const
命令,另外两种声明变量的方式:import
命令和class
命令。所以ES6 一共有 6 种声明变量的方法。函数也是对象。
使用var
和function
声明的变量会变量提升(函数名就是变量名),其他的不会被提升(隐式声明也不会)。
局部变量和全局变量 |
局部变量:在函数内部显式声明的变量(包括形参) + 块级作用域内let,const
声明的变量;其他的则为全局变量
在 Web 浏览器中,全局执行环境被认为是 window
对象,可通过window
对象访问全局变量和方法。(并非所有的全局变量都会作为全局对象的属性或方法)
显式声明与隐式声明 |
显式声明:带有声明变量关键字的
隐式声明:不带有声明变量关键字的
- 隐式声明的变量都是全局变量
- 显式声明的变量不可以通过delete删除,而隐式声明的却可以
- 隐式声明的变量不存在变量提升,只有通过
var
声明的变量才会存在变量提升 - 如果在函数内部隐式声明变量,直到函数执行后该变量才能称为全局变量
注意:只有var
声明或者隐式声明的变量才会作为window
对象的属性,命名函数function xx(){}
和使用var
声明匿名函数var xx=function(){}
也会作为window
对象的方法。
<body><input type="text" onclick="this.value=window.name" value="点击看name"/><input type="text" onclick="this.value=window.age" value="点击看age"/><input type="text" onclick="this.value=window.isSingle" value="点击看status"/><input type="text" onclick="this.value=window.sex" value="点击看sex"/><input type="text" onclick="this.value=window.animal" value="点击看animal"/><input type="text" onclick="this.value=window.profile()" value="点击看profile执行"/>
</body>
<script>var name = 'Free Joe';//显示声明(全局)age = 24;//隐式声明let animal ='dog';//let 声明的全局变量function profile(){var isSingle = true//显示声明(局部){sex = 'male';//隐式声明}return `My name is ${name},a ${age}-year-old ${sex} who is ${isSingle?'single '+animal:'in couples'}`;}profile();
</script>
📌使用name
属性作为全局变量时,需要注意的是name
属性本身也是window
对象的自带属性,所以直接使用或者打印这个变量也不会报错,只是空字符串而已。
练手:
function foo(){//console.log(a)a=1;
}
foo()
console.log(a)
试下解开注释会是什么效果?
var & let |
let
声明的变量只在 let
命令所在的代码块 {}
内有效,在 {}
外不能访问。包含有let,const
的{}
才会被提升为块级作用域。
let不允许重复声明 |
同一作用域下,let
声明的变量不能再被显示声明,var
声明的变量是可以被var
重复声明
var x = 1;
let x = 2;//SyntaxError: Identifier 'x' has already been declared
console.log(x)//报错重复声明var y = 1;
var y = 2;
console.log(y);//2
let不存在变量提升 |
同一作用域下,let
声明的变量不可以先赋值后声明
块级作用域存在变量提升,只是let,const
声明的变量不会提升而已
x=1//ReferenceError: Cannot access 'x' before initialization
let x=2
console.log(x)//报错:未初始化x前不可读取xy=1
var y=2
console.log(y)//打印:2
块级作用域 |
let
声明的变量只在块级作用域内有效
并非所有在块级作用域内声明的变量对外部作用域都是不可见的,只有使用了let,const
声明的变量对外才是不可见。
x=1
{let x=2console.log(++x)//打印:3
}
console.log(x)//打印:1
没有let,const
加持的{}
是毫无意义的,也不会形成块级作用域,类似if的代码块
{var z = 2;}
console.log(z)//打印:2
暂存性死区 |
暂存性死区是相对于某一个使用let,const
声明的变量/常量而言的,在该代码块内该变量定义之前的区域就是暂存性死区。换言之,在let,const
所在的块级作用域中,无法在let,const
之前调用其声明的标识符。
var i = 1
{
//死区开始console.log(i);//Uncaught ReferenceError: Cannot access 'i' before initializationlet i = 1;
//死区结束
}
解析:{}
内使用let
声明了变量i
,那么 {}
内的 i
标识符会被屏蔽掉直到遇到let i
才开启。所以无论你在{}
外怎么申明声明定义该标识符也无济于事。
拓展:为什么需要块级作用域? |
1.用来计数的循环变量泄露为全局变量
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
</body>
<script>var btns = document.getElementsByTagName('button')for (var i = 0; i < btns.length; i++) {btns[i].onclick = function () {console.log('第' + (i + 1) + '个')}}
</script>
无论点哪个按钮都输出 第4个
循环变量的那部分 (姑且称之为“循环变量区”)声明了变量i
,可以视为var i;i=0;
,第二次循环就是i=1
,滴三次就是i=2
,准备第四次声明,i=3
,条件不符合,没有进入循环体。当我们点击按钮的时候,触发方法,访问全局变量i
,此时i
为3,打印i+1
则为4。
如何实现点击第几个按钮就提示那个按钮呢?很简单,只要将for循环中的var改成let来声明i
即可。
用let
声明之后,for循环还有一个特别之处,循环变量区是一个父作用域,而循环体内部是一个单独的子作用域。
循环变量区let i;i=0;
之后每次循环都是给i
赋值而已,但是{}
每次循环,都会建立一个全新的块级作用域。
for (let i = 0; i < btns.length; i++) {btns[i].onclick = function () {console.log('第' + (i + 1) + '个')}}
2.内层变量可能会覆盖外层变量。
var tmp = new Date();function f() {console.log(tmp);if (false) {var tmp = 'hello world';}
}f(); // undefined
同作用域下,函数提升会高于全局变量的。函数内有自己的变量对象VO,编译阶段,tmp变量声明被提升至函数顶层作用域,只有条件为false
时候才对tmp赋值,所以输出undefined
。
const |
const
命令跟let
命令一样:不存在变量提升、具有块级作用域、存在暂时性死区、不允许重复声明 +
声明时候就必须初始化 |
const y;//SyntaxError: Missing initializer in const declaration
若值为字面量,值不可修改 |
字面量值保存在变量所指向的那个内存地址,因此等同于常量。
const x=2
console.log(++x)//TypeError: Assignment to constant variable.
若值为引用值(对象等),对象里面的属性值是可以更改的 |
变量指向的是内存地址,保存的是一个指针,const只能保存这个指针地址是固定的,至于他指向的数据就无法掌控了。
var obj={name:'FreeJoe',age:24}
const x =obj
obj.age=100
console.log(obj.age)//打印:100
当然,如果不希望属性值和属性名改变,也不希望扩充新的属性名和属性值,需要用Object.freeze()
方法
const person = {name:'Joe',age:25}
Object.freeze(person)
person.name='Tom';
person.tel='110';
console.log(person);//{name:'Joe',age:25}
|
JS 代码执行过程分为两个阶段
- 词法分析:词法分析主要包括:分析变量声明、分析函数声明、分析形参三个部分。
- 执行阶段
换而言之,变量的声明和赋值对于js执行引擎而言并非一步到位,而是先提取变量声明(变量符号表)再赋值。并非读取一条var a = 1
就会立马分两步走,而是读取整个<script>
域的var x = xx
后,把所有的变量声明剥离出来后,再赋值。
使用var关键字声明的变量 ,会在同一域(局部或全局)所有的代码执行之前被声明;但是如果声明变量时不是用var关键字,则变量不会被声明提前。
x=1;
console.log(x)//ReferenceError: Cannot access 'x' before initialization
let xconsole.log(y)//undefined
var y=1;console.log(z)//ReferenceError: z is not defined
z=1;i=1;
console.log(i)//1
var i=2
console.log(i)//2
注:JavaScript 严格模式(strict mode)不允许使用未(后)声明的变量。
var a,b;
(function(){console.log(a)//undefinedconsole.log(b)//undefinedvar a=b=1console.log(a)//1console.log(b)//1
}())
console.log(a)//undefined
console.log(b)//1
不要被var a=b=1
这些连等式迷惑,拆解出来,var a=b,b=1
。此处b
是全局变量,而a只是局部变量。
|
具名函数的声明有两种方式:1. 函数声明式 2. 函数表达式(匿名方式)
//函数声明式
function bar () {}
//函数表达式声明;
var foo = function () {}
函数表达式中的函数不会被提升,函数声明会被提升。
声明式函数会提升到做顶层作用域
console.log(sub)//undefined
console.log(sub(1))//TypeError: sub is not a function
var sub =function(a){return --a}console.log(add(1))//2
function add(a){return ++a}
如果有变量和函数同名,则会忽略掉变量,只提升函数
console.log(foo)
var foo=1
function foo(){}//[Function: foo]
console.log(foo)//1
|
尽可能少用全局变量
容易造成命名污染,也一定程度影响内存释放。
优先推荐使用const
-> let
最不推荐var
在let
和const
之间,建议优先使用const
,尤其是在全局环境,不应该设置变量,只应设置常量。
JavaScript 编译器会对const
进行优化,所以多使用const
,有利于提高程序的运行效率。
const
声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。
他们没有明确说明为什么要这样做,但我认为这样做有以下好处:
1.加强语义化 (常量与变量) 更好的可读性
2.避免for(var ) 循环计数变量泄露为全局变量类似的问题。