一.函数作用域和块作用域
1.1函数中的作用域
- 函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(其实在嵌套的作用域中也可以使用)。
1.2隐藏内部实现
①是什么?
- 挑选出函数内部的一个任意的代码片段,然后用函数声明对它进行包装,实际上就是将这些代码"隐藏"起来了。
②对原始函数带来的影响?
- 在原始函数内部嵌套了一个新的包装函数的作用域。
③那么,为什么要选择将一个函数作用域隐藏于另一个函数作用域内部呢?
- 最小特权原则:在软件设计中,应该最小限度地暴露必要内容,而将其他内容都"隐藏"起来。
function doSomething(a){
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a){
return a-1;
}
var b = 0;
doSomething(2);
>>>15
function doSomething(a){
function doSomethingElse(a){
return a - 1;
}
var b = 0;
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
doSomething(2);
>>>15
④规避冲突
- "隐藏"作用域中变量和函数所带来的另一个好处是,可以避免同名标识符之间的冲突,标识符同名但用途却不一样,可能会造成命名冲突,最终会导致变量的值被意外覆盖:
function foo(){
function bar(a){
i = 3;//修改了for循环所属属性的i
console.log(a + i);
}
for(var i = 0;i < 10;i++){
bar(i*2);//因为i的值被修改为3,所以永远都小于10,导致无限循环!
}
}
foo();
//解决
function foo(){
function bar(a){
var i = 3;//"遮蔽变量"
console.log(a + i);
}
for(var i = 0;i < 10;i++){
bar(i*2);
}
}
foo();
另外两种解决方式:
1.全局命名空间
var MyReallyCoolLibrary = {
awesome: 'stuff',
doSomething: function(){
// ...
},
doAnotherThing: function(){
//...
}
};
2.模块管理
- 使用模块管理器,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另一个特定的作用域中。
1.3函数作用域
①
var a = 2;
var a = 3;
console.log(a);
>>>3
②使用所学内容:"隐藏内部实现",将最后两行代码"隐藏"掉:
var a = 2;
function foo(){
var a = 3;
console.log(a);//3
}
foo();
console.log(a);//2
③虽然解决了部分问题,但也带来了其他的麻烦:
- 必须要声明一个具名函数
foo()
,导致其函数名"污染"了所在作用域(这里是全局作用域)。 - 必须显式通过函数名来调用这个函数才能运行其中的代码。
④解决之法:
var a = 2;
(function foo(){
var a = 3;
console.log(a);//3
})();
console.log(a);//2
⑤如何区分函数声明和函数表达式?
- 最简单的方法就是看
function
关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明的位置)
function foo(){...} //函数声明
var test = function(){...} //函数表达式
(function foo(){...})() //函数表达式
- 若function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
⑥函数声明和函数表达式的区别?
两者最主要的区别在于它们的名称标识符会绑定在何处。
(function foo(){ ... })
作为函数表达式意味着foo
只能在...
所代表的位置中被访问,外部作用域则不行。foo
变量名被隐藏在自身中意味着不会"污染"外部作用域。
1.4块作用域
①let
关键字,除了var
以外的另一种变量声明的方式,用于在任何代码块中声明变量。
②存在的原因?
-
let
关键字可以将变量绑定到所在的任意作用域中(通常是{...}
内部)。
if(true){
var b = 20;
}
console.log(b);
>>>20
if(true){
let b = 20;
}
console.log(b);
>>>Uncaught ReferenceError: b is not defined
③提升是什么?
- 在某个作用域任意位置的声明,都会被"转移"到该作用域的顶部。但是使用
let
进行的声明不会在块作用域中进行提升!
console.log(a);
var a = 2;
>>>2
{
console.log(a);
let a = 2;
}
>>>Uncaught ReferenceError: a is not defined
④
for(let i = 0;i < 5;i++){
}
console.log(i);
>>>Uncaught ReferenceError: i is not defined
⑤除了let
之外,ES6还引入了const
,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改其值的操作都会导致错误(Uncaught TypeError:Assignment to constant variable)
。
if(true){
const b = 5;
}
console.log(b);
>>>Uncaught ReferenceError: b is not defined
if(true){
const b = 5;
b = 10;
}
>>>Uncaught TypeError: Assignment to constant variable.
二.提升
1.所有声明(变量+函数)都会在任何代码被执行前,首先被编译器处理。
var a = 2;
//分解为两个阶段
//第一个声明:定义声明,在编译阶段进行
var a;
//第二个声明:赋值声明,原地待命,等待执行阶段
a = 2;
2.先有鸡还是先有蛋?
- 反正JavaScript里是先有声明再有赋值。
3.只有定义声明会被提升,而赋值声明或其他运行逻辑则会原地待命。
4.每个作用域都会进行提升操作:
foo();
function foo(){
console.log(a);//undefined
var a = 2;
}
//其实是这样的
function foo(){
var a;
console.log(a);//undefined
a = 2;
}
foo();
5.
foo();// 不是ReferenceError,而是TypeError!
var foo = function bar(){
// do something
};
//还原一下
var foo;
foo();//TypeError
foo = function bar(){
// do something
};
//More
var foo;
foo();//Uncaught TypeError: foo is not a function
foo = function bar(){
// do something
};
①ReferenceError
和TypeError
的区别?
如果
RHS
查询(找到变量并获取其值)在任何作用域中都查找不到所需的变量,引擎就会抛出ReferenceError
异常。如果
RHS
查询找到了所需变量,但是当你对该变量的值进行不合理的操作时,比如试图对一个非函数类型的值进行函数调用,或者引用null
或undefined
类型的值中的属性,那么引擎就会抛出另外一种类型的异常,叫做TypeError
。RHS:Retrieve His Source.
6.即使是具名的函数表达式,名称标识符在被赋值之前也无法在所在作用域中使用:
foo();//TypeError
bar();//ReferenceError
var foo = function bar(){
// do something
};
//还原一下
var foo;
foo();//TypeError
bar();//ReferenceError,压根在全局作用没有找个这个函数
foo = function(){
var bar = ...self...
// do something
};
7.函数优先
①函数声明和变量声明都会被提升,但是有个先来后到,函数会首先被提升,然后才是变量。
foo();
var foo;
function foo(){
console.log('我是foo的函数声明');
}
foo = function(){
console.log('我是foo的函数表达式');
};
>>>我是foo的函数声明
//还原一下
function foo(){
console.log('我是foo的函数声明');
}
foo();
//var foo;被同名的foo函数声明"覆盖"掉
foo = function(){
console.log('我是foo的函数表达式');
};
>>>我是foo的函数声明
②尽管重复的var
声明会被忽略掉,但出现在后面的函数声明还是可以覆盖掉前面:
//后面的函数声明
foo();
function foo(){
console.log('我是foo的函数声明');
}
function foo(){
console.log('我是后面的函数声明');
}
>>>我是后面的函数声明
③杜绝在同一个作用域进行重复定义。