02-深入变量 与 闭包
码路教育 6/22/2022
# 1. 加var的变量和不加var的变量
加var的变量和不加var的变量有什么区别:
- 加var的变量在预编译期间会提升,不加var的变量在预编译期间不会提升。
<script>
// 加var的变量在预编译期间会提升,不加var的变量在预编译期间不会提升。
// console.log(a); // undefined
// var a = 110;
console.log(b); // ReferenceError: b is not defined
b = 120;
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 不管有没有加var,创建的全局变量,都会放到GO中的,也就是可以通过window.xx。
<script>
// 不管有没有加var,创建的全局变量,都会放到GO中的,也就是可以通过window.xx。
var a = 110;
b = 120;
console.log(window.a); // 110
console.log(window.b); // 120
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
加var的变量,可能是全局变量,也可能是局部变量,不加var的变量,只能是全局变量。
加var的局部变量,不会作用window的属性
<script>
function fn(){
var a = 110;
}
fn();
console.log(window.a);
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
不加var创建的变量:
- 不建议使用。 项目中尽量不要使用全局变量。在项目中,不要使用没有加var的变量。
# 2. let声明的变量
使用let声明的变量的特点:
- 使用let声明的变量没有提升。针对这个错误:ReferenceError: Cannot access 'a' before initialization有人是这样说的:let声明的变量也有提升。但是没有赋值(没有初始化),就记一种:使用let声明的变量也提升,只不过没有初始化,去使用的时候就报错了。
<script>
// 使用let声明的变量没有提升
// initialization 初始化的意思
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 110; // let也可以声明变量
</script>
1
2
3
4
5
6
2
3
4
5
6
- 使用let配合{}会形成块级作用域。 在C语言中,就有块级作用域。在JS中,原本是没有块级作用域。在块中声明的变量,只能在块中使用。
<script>
// 在let之前,只有两种作用域 1)全局作用域 2)函数内部的局部作用域
var a = 110; // a处于全局作用域中
console.log(a);
function fn(){
var b = 220; // b处于局部作用域中
console.log(b);
}
fn();
// 在let之前,是没有块级作用域 {}
if(true){ // if 后面的 {} 在其它编程语言中会形成块级作用域
// {}如果是块级作用域 c在{}外面是不能访问的
var c = 330; // c是全局变量
}
console.log(c);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
if(true){
// 此时 let + {} 就会形成块级作用域
// 在此块定义的数据,只能在此块中访问
let a = 110;
}
console.log(a);
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 使用let声明的变量并不会挂载到GO中
<script>
let a = 110;
console.log(window.a);
</script>
1
2
3
4
2
3
4
- 使用let声明的变量也不能重复声明
<script>
var a = 1;
var a = 2;
var a = 3;
console.log(a);
let b = 1;
let b = 2;
let b = 3;
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
需要知道:项目中不要使用var来声明变量,你要声明变量,使用let。
使用var声明变量不足:
- 提升
- 重复声明 浏览器中有这样一个机制,如果一个变量提升过了,后面遇到同名变量,就不会提升了。
# 3. const声明变量(常量)
使用const声明的变量(常量)的特点:
- 声明的变量不能修改(常量)
<script>
const a = 110;
a = 120;
console.log(a);
</script>
1
2
3
4
5
2
3
4
5
- 声明时必须赋值(初始化) 定义 = 声明+赋值(初始化)
<script>
const a;
a = 110;
console.log(a);
</script>
1
2
3
4
5
2
3
4
5
- const声明的常量也不会提升。
- const声明的常量配合{} 也会形成块级作用域
- const声明的常量也不会挂载到GO上。
总结:项目中不要使用var,如果声明变量使用let,如果声明常量,使用const。
# 4. 练习题(面试题)
<script>
// 使用之前学的知识点来做
// fn函数整体提升了
console.log(fn) // fn函数
window.fn(); // fn...
console.log(window.fn); // fn函数
if("fn" in window){
fn(); // fn...
function fn(){
console.log("fn...");
}
}
fn();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 在很早的浏览器中,运行出来的结果,就是上面分析出来的结果
// 但是现在的浏览器结果不一样了
// 如果是全局函数 并且这个函数放到if中 它提升的不再是整体提升
// 它提升的仅仅是函数名,整体函数体是没有提升的
// 在预编译期间,如果函数声明放到if中,仅仅是提升函数名,不提升函数体
console.log(fn) // fn函数名提升了,默认值和变量是一样的,是und
window.fn(); // und不能加()去调用
console.log(window.fn); //
if("fn" in window){
fn(); //
function fn(){
console.log("fn...");
}
}
fn();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
// 在很早的浏览器中,运行出来的结果,就是上面分析出来的结果
// 但是现在的浏览器结果不一样了
// 如果是全局函数 并且这个函数放到if中 它提升的不再是整体提升
// 它提升的仅仅是函数名,整体函数体是没有提升的
// 在预编译期间,如果函数声明放到if中,仅仅是提升函数名,不提升函数体
console.log(fn) // und
console.log(window.fn); // und
// 问:GO中有没有fn 答:有的,只不过它的值是und
if("fn" in window){
// 如果if条件成立,进入到if中的第1件事,就是给fn赋值
fn(); // fn...
function fn(){
console.log("fn...");
}
}
fn();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
// 预编译:加var变量和function声明的函数整体
// function fn(){ console.log(1); } 要提升 fn是一个函数 log(1)
// function fn(){ console.log(2); } 已经提升过一次fn了, fn要重新赋值 log(2)
// var fn; 进提升 fn值又变成了und
// function fn(){ console.log(4); } fn的又变成了函数 log(4)
// function fn(){ console.log(5); } fn的又要重新赋值 log(5)
// fn 函数 log(3)
fn(); // 5
function fn(){ console.log(1); }
fn(); // 5
function fn(){ console.log(2); }
fn(); // 5
var fn = function(){ console.log(3); }
fn(); // 3
function fn(){ console.log(4); }
fn(); // 3
function fn(){ console.log(5); }
fn(); // 3
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
// var a = 12; var b = 13; var c = 14;
var a = 12; b = 13; c = 14;
function fn(a){
// 形参a是函数内部的局部变量a 形参a的值是10
console.log(a,b,c); // 10 13 14
a = 100; // 找a 把100赋值给了局部变量a
b = 200; // 找b 把200赋值给外面的b 是全局变量b
console.log(a,b,c); // 100 200 14
}
b = fn(10); // und
console.log(a,b,c); // 12 und 14
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
<script>
var a = 12; b = 13; c = 14;
function fn(a){
console.log(a,b,c);
a = 100;
b = 200;
console.log(a,b,c);
}
b = fn(10);
console.log(a,b,c);
</script>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
<script>
// var a = 1;
// let a = 2; // Identifier 'a' has already been declared
function sum(a){
// 形参是相当于函数内部局部变量
console.log(a); // 报错了
let a = 100; //
console.log(a); //
}
sum(200);
</script>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
<script>
var ary = [12,13];
function fn(ary){
console.log(ary);
ary[0] = 100;
ary = [100];
ary[0] = 0;
console.log(ary);
}
fn(ary);
console.log(ary);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
<script>
function fn(){
function gn(){
console.log("gn...");
}
return gn;
}
var r = fn();
r();
</script>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
<script>
function fn() {
// 直接返回一个函数,返回的也是地址
return function gn() {
console.log("gn...");
}
}
var r = fn();
r();
</script>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
<script>
var i=0;
function A(){
var i=10;
function x(){
console.log(i);
}
return x;
}
var y = A();
y();
function B(){
var i=20;
y();
}
B();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
var i = 5;
function fn(i){
return function(n){
console.log(n+(++i))
}
}
var f = fn(1);
fn(2);
fn(3)(4);
fn(5)(6);
f(7);
console.log(i);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5. JS中函数是一等公民
在JavaScript中,函数是非常重要的,并且是一等公民:
- 那么就意味着函数的使用是非常灵活的;
- 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;
什么是高阶函数:
一个函数它的参数是函数或它的返回值是函数,那么这个函数就是高阶函数。
自己编写高阶函数
<script>
// func是形参 把gn这个函数传递给func
// gn中保存的是地址 地址传递给func
function fn(func){
func();
}
function gn(){
console.log("gn...");
}
// gn叫实参
fn(gn)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
<script>
function add(num1,num2){ return num1+num2 }
function sub(num1,num2){ return num1-num2 }
function mul(num1,num2){ return num1*num2 }
// 封装一个计算器函数
function calc(num1,num2,fn){
console.log(fn(num1,num2));
}
calc(10,20,add);
calc(10,20,sub);
calc(10,20,mul);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
<script>
// fn也是高阶函数
function fn(){
function gn(){
console.log("gn");
}
return gn; // 返回一个地址
}
let kn = fn();
kn();
</script>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
<script>
// 高阶函数:如果一个函数接受另外一个函数作为参数,或者该函数会返回另外一个函数作为返回值,
// 那么这个函数就叫高阶函数
// 闭包可以延长变量的生命周期
function fn(count){
function add(num){
return count + num;
}
return add;
}
var gn5 = fn(5);
console.log(gn5(6));
console.log(gn5(10));
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
- 使用内置的高阶函数
<!-- 使用传统的方式过滤偶数,如下: -->
<script>
let nums = [10,3,12,50,99,30];
// 把上面数组中的偶数过滤出来
let newArr = [];
// 命令式编程
for(var i=0; i<nums.length; i++){
if(nums[i] % 2 == 0){
newArr.push(nums[i])
}
}
console.log(newArr);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 可以使用高阶函数,如下: -->
<script>
// ---------------------- filter 过滤
let nums = [10,3,12,50,99,30];
// function(item){} 整体是实参
// filter 是JS中内置好的高阶函数
// item 这个item表示数组中的每一项
// 不要问为什么,JS就是这规定
// 声明式编程
let newArr = nums.filter(function(item){
// console.log(item);
// return true; // 把item每一项都放到新数组中
return item % 2 == 0;
})
console.log(newArr); // filter返回值是一个数组
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
// ---------------------- map 映射
let nums = [10,3,12,50,99,30];
let newArr = nums.map(function(item){
// 可以对item每一项数据进行加工
return item*10
})
console.log(newArr); // map返回值是一个新数组
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<script>
// ---------------------- forEach 遍历
let nums = [10, 3, 12, 50, 99, 30];
nums.forEach(function(item){
console.log(item);
})
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
<script>
// ---------------------- find 查找
let nums = [10, 3, 12, 50, 99, 30];
var item = nums.find(function(item){
return item === 30
})
console.log(item);
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<script>
// ---------------------- findIndex
let nums = [10, 3, 12, 50, 99, 30];
// 返回找到元素的索引
var item = nums.findIndex(function(item){
return item === 30
})
console.log(item);
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<script>
// ---------------------- find 查找
let names = [
{name:"wc",age:10},
{name:"xq",age:11},
{name:"z3",age:12},
{name:"L4",age:13},
];
var res = names.find(function(item){
// item表示数组中的每一项,是里面的对象
return item.name === 'z3'
})
console.log(res);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6. 什么是闭包
什么是闭包:
- 函数执行后返回结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,即形成了闭包。
闭包是一片不会被销毁的EC。它对EC中的数据有保护和保存的作用:
- 保护:防止像全局变量那样被污染,数据在函数内部,外界访问不了。
- 保存:可以像全局变量那样,延长变量的生命周期。
- 闭包使用不当可能会导致内存泄露
应用场景:
- 模块封装,在各模块规范(ES6)出现之前,都是用这样的方式防止变量污染全局
- 数据私有化
- 封装一些工具函数
- 在循环中创建闭包,防止取到意外的值。
<script>
var i = 20;
function fn(){
i -= 2;
var i = 10;
return function(n){
console.log((++i)-n);
}
}
var f = fn();
f(1);
f(2);
fn()(3);
console.log(i);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
let a = 0;
b = 0;
function A(a){
A = function(b){
// b++ 优先级高于 +
alert(a+b++);
}
alert(a++)
}
A(1); // 1
A(2); // 4
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
<script>
var t = (function(i){
return function(){
alert(i *= 2)
}
})(2);
t(5);
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<script>
var n = 0;
function a(){
var n = 10;
function b(){
n++;
console.log(n);
}
b();
return b;
}
var c = a();
c();
console.log(n);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 7. IIFE
Immediately Invoked Function Expression(立即调用函数表达式)
<script>
function fn(){
console.log("fn...");
}
// 之前:先定义函数 再去调用函数
fn();
// 思考:在定义函数时,能不能直接去调用
// 答:IIFE
</script>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
<script>
function fn(){
console.log("fn...");
}()
</script>
1
2
3
4
5
2
3
4
5
- 正确的IIFE,如下:
<script>
// IIFE写法一
(function fn() {
console.log("fn...");
})()
</script>
<script>
// IIFE写法二
(function fn() {
console.log("fn...");
}())
</script>
<script>
// IIFE写法三
+function fn() {
console.log("fn...");
}()
</script>
<script>
// IIFE写法四
-function fn() {
console.log("fn...");
}()
</script>
<script>
// IIFE写法五
!function fn() {
console.log("fn...");
}()
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 注意问题:
<script>
let obj = {
name:"wc",
age:100
}
(function(){
console.log("mn...");
})();
</script>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
<script>
let obj = {
name:"wc",
age:100
};
(function(){
console.log("mn...");
})();
</script>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 为了防止,你前面的代码没有加分号,通常写一个IIFE,都会在前面加上分号,如下:
<script>
let obj = {
name:"wc",
age:100
}
// 在写IIFE时,直接以分号开头
;(function(){
console.log("mn...");
})();
</script>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10