作用域
ES5中,
函数作用域:在JS函数中声明的变量,会成为函数的局部变量,函数内部声明的变量外部无法访问。
全局作用域:函数外声明的变量,会成为全局变量,在函数内部也可以访问。
当函数嵌套,在这个时候,内部函数与外部函数的这个变量就组成了闭包。
注意点:在ES5中没有块级作用域
-
var a = 10;
-
function foo() {
-
// 当函数内部有变量a时,会产生局部作用域,外界全局作用域中的a不会对函数内部造成影响
-
console.log(a); //undefined
-
var a = 100;
-
// 如果把函数内部的变量a注释掉是,函数内部的a输出的就是全局作用域中的a = 10
-
console.log(a); // 100
-
-
function fn() {
-
console.log(a); //undefined
-
var a = 200;
-
console.log(a); // 200
-
}
-
fn()
-
}
-
foo()
'
运行
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突
作用域链
首先要知道什么是*变量
*变量就是当前作用域没有定义的变量,那么*变量的值如何得到 —— 要到创建这个函数的那个父级作用域寻找,如果没有就一直向上级祖先元素寻找(这就是所谓的"静态作用域",静态作用域是指函数的作用域在函数定义时就已经确定。
-
var a = 100
-
function fn() {
-
var b = 200
-
console.log(a) // 这里的a在这里就是一个*变量 // 100
-
console.log(b)
-
}
-
fn()
'
运行
那么什么是作用域链呢
如果父级也没有这个变量呢?那就继续一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。
立即执行函数 IIFE
作用
页面加载完成后只执行一次的设置函数。
将设置函数中的变量包裹在局部作用域中,不会泄露成全局变量。
IIFE的使用:
-
// 这是不采用IIFE时的函数声明和函数调用:
-
function foo(){
-
var a = 10;
-
console.log(a);
-
}
-
foo();
-
//如果普通函数和IIFE函数现在一起 记得使用;进行分隔
-
-
// 下面是IIFE形式的函数调用:
-
(function foo(){
-
var a = 10;
-
console.log(a);
-
})();
'
运行
区别:
在函数的声明中,我们首先看到的是function关键字,而IIFE我们首先看到的是左边的( ,也就是说,使用一对()将函数的声明括起来,使得JS编译器不再认为这是一个函数声明,而是一个IIFE,即需要立刻执行声明的函数。两者达到的目的是相同的,都是声明了一个函数foo并且随后调用函数foo。
IIFE的写法
1.对返回结果不进行处理
-
(function(形参){
-
函数体内容
-
})(实参);
2.对返回结果不进行处理
-
(function(形参){
-
函数体内容
-
}(实参));
以上两种用法是比较常用的,区别是后一个括号的位置。
3.返回的是一个布尔值,然后进行取反
-
!function(形参){
-
return true
-
}(实参)
4.对于数字返回的是原来的结果,非数字返回NaN
-
+function(形参){
-
return 123
-
}(实参)
5.对于数字返回的是正负符号相反,非数字返回NaN
-
-function(形参){
-
return 123
-
}(实参)
6.对于数字返回的是正负符号相反再减1,非数字返回-1
-
~function(形参){
-
return 123
-
}(实参)
7.返回的结果是undefined
-
void function(形参){
-
函数体内容
-
}(实参)
IIFE的基本使用
-
// 就像其它任何函数一样,一个立即执行函数也能返回值并且可以赋值给其它变量。
-
var sum = (function (a,b) {
-
return a + b;
-
}(1,2))
-
console.log(sum);
'
运行
!!!经典面试题
为什么需要IIFE?
如果只是为了立即执行一个函数,显然IIFE所带来的好处有限。实际上,IIFE的出现是为了弥补JS(ES5)在作用域方面的缺陷:JS只有全局作用域(global scope)、函数作用域(function scope),从ES6开始才有块级作用域(block scope)。在JS中为了实现作用域的隔离,只有function才能实现作用域隔离,因此如果要将一段代码中的变量、函数等的定义隔离出来,只能将这段代码封装到一个函数中。在我们通常的理解中,将代码封装到函数中的目的是为了复用。在JS中,当然声明函数的目的在大多数情况下也是为了复用,但是JS迫于作用域控制手段的贫乏,我们也经常看到只使用一次的函数:这通常的目的是为了隔离作用域了!既然只使用一次,那么立即执行好了!既然只使用一次,函数的名字也省掉了!这就是IIFE的由来。
闭包
什么是闭包?
简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数,函数嵌套函数,内部引用外部。
MDN :闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
闭包的生成的必要条件:
-
函数嵌套函数
-
内部函数引用了外部函数中的数据(属性、函数)
-
参数和变量不会被回收
如何产生闭包?
在一个函数内部创建另外一个函数
-
function func() {
-
var a = 1, b = 2;
-
-
function closure() {
-
return a + b;
-
}
-
return closure;
-
}
-
console.log(func()()); // 3
'
运行
闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。
在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
用途:
- 可以读取函数内部的变量,
- 让这些变量的值始终保持在内存中。
案例:
-
function f1() {
-
var n = 999;
-
nAdd = function () { n += 1 }
-
function f2() {
-
console.log(n);
-
}
-
return f2;
-
}
-
var result = f1();
-
result(); // 999
-
nAdd();
-
result(); // 1000
'
运行
解析:
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
闭包使用注意点:
- 闭包会使函数中的变量都保存在内存中不会被销毁,因此会消耗很大的内存空间,滥用闭包会导致网页的性能问题,也有可能造成内存泄露(IE)。解决方法:在退出函数时将不需要的局部变量全部删除。
- 闭包会在父函数我不改变父函数内部的值。如果将父函数当作对象使用,把闭包当作公用方法,把内部变量当作它的私有属性,那么不要随便修改父函数内部变量的值,当父函数变量被修改时,所有子函数都会被影响。
-
<!DOCTYPE html>
-
<html lang="en">
-
<head>
-
<meta charset="UTF-8">
-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>Document</title>
-
</head>
-
<body>
-
<script>
-
var name = 'window-name'
-
var obj = {
-
name: 'obj-name',
-
say: function () {
-
return function () {
-
console.log(this.name);
-
}
-
}
-
}
-
var x = obj.say()
-
// 只看谁调用了x(),由于在全局作用域下被调用,因此this指向全局变量window
-
x() // window-name
-
</script>
-
</body>
-
</html>