一、作用域
一般将作用域分成:全局作用域、函数作用域、块级作用域
全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问
// 全局变量
var greeting = 'Hello World!';
function greet() {
console.log(greeting);
}
// 打印 'Hello World!'
greet();
函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);
块级作用域
ES6引入了块级作用域。块级作用域就是指变量在指定的代码块里面才能访问,也就是一对{}
中可以访问,在外面无法访问。为了区分之前的var
,块级作用域使用let
和const
声明,let
声明变量,const
声明常量。
function f() {
let y = 1;
if(true) {
var x = 2;
let y = 2;
}
console.log(x); // 2
console.log(y); // 1
}
f();
解释:x(var声明的 变量是函数作用域,即在函数f内)、y(let声明的是块级作用域,第二个y作用域在if的{}内)
不允许重复申明
块级作用域在同一个块中是不允许重复声明的
//直接报错Uncaught SyntaxError: Identifier 'a' has already been declared
var a = 1;
let a = 2;
//var可以重复声明,不报错
//var a = 1;
//var a = 2;
//注意的是相同作用域,下面这种情况是不会报错的
let a = 20
{
let a = 30
}
变量提升说明
let
和const
申明的变量不会提升这种说法是不准确的
var x = 1;
if(true) {
console.log(x);
let x = 2;
}
解释:报错Uncaught ReferenceError: Cannot access 'x' before initialization
。如果let
申明的x
没有变量提升,那我们在他前面console
应该拿到外层var
定义的x
才对。但是现在却报错了,说明执行器在if
这个块里面其实是提前知道了下面有一个let
申明的x
的,所以说变量完全不提升是不准确的。只是提升后的行为跟var
不一样,var
是读到一个undefined
,而块级作用域的提升行为是会制造一个暂时性死区(temporal dead zone, TDZ)。暂时性死区的现象就是在块级顶部到变量正式声明这块区域去访问这个变量的话,直接报错,这个是ES6规范规定的。
JS是静态作用域
let x = 10;
function f() {
return x;
}
function g() {
let x = 20;
return f();
}
console.log(g()); // 10
解释:我们调用一个函数时,如果这个函数的变量没有在函数中定义,就去定义该函数的地方查找
相似题:
var n=123;
function f1(n){
console.log(n);
}
function f2(){
var n=456;
f1(n);//456
}
f2();
console.log(n)//123
作用域链
作用域链其实是一个很简单的概念,当我们使用一个变量时,先在当前作用域查找,如果没找到就去他外层作用域查找,如果还没有,就再继续往外找,一直找到全局作用域,如果最终都没找到,就报错。比如如下代码:
let x = 1;
function f() {
function f1() {
console.log(x);
}
f1();
}
f();
这段代码在f1
中输出了x
,所以他会在f1
中查找这个变量,当然没找到,然后去f
中找,还是没找到,再往上去全局作用域找,这下找到了。这个查找链条就是作用域链。
作用域链延长
前面那个例子的作用域链上其实有三个对象:
f1作用域 -> f作用域 -> 全局作用域
大部分情况都是这样的,作用域链有多长主要看它当前嵌套的层数,但是有些语句可以在作用域链的前端临时增加一个变量对象,这个变量对象在代码执行完后移除,这就是作用域延长了。能够导致作用域延长的语句有两种:try...catch
的catch
块和with
语句。
try...catch
这其实是我们一直在用的一个特殊情况:
let x = 1;
try {
x = x + y;
} catch(e) {
console.log(e);
}
上述代码try
里面我们用到了一个没有声明的变量y
,所以会报错,然后走到catch
,catch
会往作用域链最前面添加一个变量e
,这是当前的错误对象,我们可以通过这个变量来访问到错误对象,这其实就相当于作用域链延长了。这个变量e
会在catch
块执行完后被销毁。
声明提前问题
变量声明提前
在ES6之前,我们声明变量都是使用var,使用var声明的变量都是函数作用域,即在函数体内可见,这会带来的一个问题就是声明提前。
var x = 1;
function f() {
console.log(x);//undefined
var x = 2;
}
f();
//等价于
var x = 1;
function f() {
var x
console.log(x);
x = 2;
}
f();
函数声明提前
//成功调用
function f() {
x();
function x() {
console.log(1);
}
}
f();
//等价于
function f() {
function x() {
console.log(1);
}
x();
}
f();
但是,如果x
函数如果换成函数表达式就不一样了
function f() {
x();
var x = function() {
console.log(1);
}
}
f();
解释:报错Uncaught TypeError: x is not a function,x
其实就是一个普通变量,只是它的值是一个函数,它虽然会提前到当前函数的最顶部声明,但是这时候他的值是undefined
,将undefined
当成函数调用,肯定就是TypeError
变量声明和函数声明提前的优先级
函数申明的优先级更高
(function e(num){
console.log(num);//f num(){}
var num=10;
function num(){}
})(100)
function b(){
//预编译:a=undefined——>a=f a(){}
console.log(a);//f a(){}
var a=10;
function a(){}
a=100;
console.log(a);//100
}
b()
再看个例子
function test(arg) {
// 1. 形参 arg 是 "hi"
// 2. 因为函数声明比变量声明优先级高,所以此时 arg 是 function
console.log(arg);
var arg = "hello"; // 3.var arg 变量声明被忽略, arg = 'hello'被执行
function arg() {
console.log("hello world");
}
console.log(arg);
}
test("hi");
/* 输出:
function arg(){
console.log('hello world')
}
hello
*/
解析:
当函数执行的时候,首先会形成一个新的私有的作用域,然后依次按照如下的步骤执行:
- 如果有形参,先给形参赋值
- 进行私有作用域中的预解释,函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重新赋值
- 私有作用域中的代码从上到下执行
if判断语句的变量提升解答
function m(){
console.log(a1)//undefined
console.log(a2)//undefined
console.log(b1)//undefined
console.log(b2)//undefined
if(false){
function b1(){}
var a1=100;
}
if(true){
function b2(){}
var a2=10;
}
console.log(a1)//undefined
console.log(a2)//10
console.log(b1)//undefined
console.log(b2)//f b2(){}
}
m()
预解析步骤:
1、变量:代码运行之前,先扫描有没有带var关键字的变量名,有的话,为这个变量名,在内存里开一个空间;预解释是发生在代码执行前的,所以if根本阻挡不了预解析;
2、函数声明:在新版本的浏览器中,写在逻辑判断语句块中的函数会当作表达式来处理,不会当作函数声明,所以也不存在函数提升的问题
循环语句中的问题
for(var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
})
}
依次输出:3 3 3
解释:因为setTimeout
是异步代码,会在下次事件循环执行,而i++
却是同步代码,而全部执行完,等到setTimeout
执行时,i++
已经执行完了,此时i
已经是3了
for(let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
})
}
依次输出:0 1 2
再来个特殊的例子
for(var i={j:0};i.j <5;i.j++){
//深拷贝后,重新开拓空间,i指向另一个空间
(function(i){
setTimeout(function(){console.log(i.j)},0);
})(JSON.parse(JSON.stringify(i)));
}
输出:0,1,2,3,4
分析:
JSON.parse(JSON.stringfy(object))进行深拷贝,结合另一篇博文题6关于函数传参进行理解。
如果这样写
for(var i=0;i<5;i.j++){
(function(i){
setTimeout(function(){console.log(i.j)},0);
})(i);
}
输出:4,4,4,4,4
真的是搞死人的题,又来了
这道题没看懂其实,为什么第1个setTimeout输出不是another executed.
var execFunc = function(){
console.log("executed");
};
setTimeout(execFunc,0);//executed
console.log("changed");//changed
execFunc = function(){
console.log("another executed");
}
setTimeout(execFunc,0); //another executed
var color='red';
setTimeout(()=>{
console.log(color);//blue
})
color='blue'
setTimeout(()=>{
console.log(color);//blue
})
这种写法也适用于for...in
和for...of
循环:
let obj = {
x: 1,
y: 2,
z: 3
}
for(let k in obj){
setTimeout(() => {
console.log(obj[k])
})
}
link:for(const i = 0; i < 3; i++)
来说,const i = 0
是没问题的,但是i++
肯定就报错了,所以这个循环会运行一次,然后就报错了。对于for...in
和for...of
循环,这里换成使用const
声明也是没问题的。