文章可能有点长,但 🌰 栗子🌰 都很经典,简单易懂,一起学习吧!
壹 ❀ 作用域
1、作用域就是一个变量或者函数的有效作用范围,在JS中作用域一共有三种,分别是全局作用域、局部作用域(函数作用域)、块级作用域;
2、变量声明的三种方式:var、let、const
1. 全局作用域
1、全局作用域:声明在函数外部的变量(声明在script标签中的变量和函数),在代码中任何地方都能访问到的对象拥有全局作用域;var和let变量在全局作用域中都是全局变量;
注意:所有没有var直接赋值的变量都属于全局变量;
2、全局作用域中声明的变量和函数会作为window对象的属性和方法保存,可以通过 window.变量名 去调用它;
3、全局作用域在页面打开时被创建,页面关闭时被销毁;
(1)最外层函数和在最外层函数外面定义的变量拥有全局作用域,例如:
var authorName="波妞";
function doSomething(){
var blogName="中介";
function innerSay(){
console.log(blogName);
}
innerSay();
}
console.log(authorName); //波妞
doSomething(); //中介
console.log(blogName); //错误
innerSay() //错误
(2)所有末定义直接赋值的变量自动声明为拥有全局作用域,例如:
function doSomething(){
var authorName="波妞";
blogName="中介";
console.log(authorName);
}
doSomething(); // 波妞
console.log(blogName); //中介
console.log(authorName); //错误
2. 局部作用域(又称 函数作用域)
在函数中用
var
、let
、const
声明的所有变量,都是函数的局部变量,作用范围为局部作用域,即:只能在函数内部使用,函数外部使用是不行的。无论是通过var还是let定义在局部作用域的变量都是局部变量;1、调用函数时,函数作用域被创建;函数执行完毕,函数作用域被销毁;
2、每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的;
3、在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量;
4、在函数作用域内访问变量/函数时,会现在自身作用域中查找,若没有找到,则会到函数的上一级作用域中寻找,一直到全局作用域;
注意:所有没有var直接赋值的变量都属于全局变量
3. 块级作用域
任何一对花括号{ }中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
在ES6中只要 { } 没有和函数结合在一起,那么就是“块级作用域”。ES6之前没有块级作用域。使用
let
关键字或者const
关键字来实现块级作用域。
let
和const
声明的变量只在let
或const
命令所在的代码块 { } 内有效,在 { } 之外不能访问。注意:
- 块级作用域中,var定义的变量是全局变量,let定义的变量是局部变量;
- 块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。但函数funtion(){ }里面的{ }不属于块级作用域,而是局部作用域(函数作用域);
4.块级作用域和局部(函数)作用域区别
4.1、在块级作用域中通过var定义的变量是全局变量
{
//块级作用域
var num = 123;//全局变量
}
console.log(num); // 123
4.2、在局部作用域中通过var定义的变量是局部变量
function test() {
var value = 666;//局部变量
console.log(value);
}
test(); // 666
console.log(value);//value is not defined
5、无论是在块级作用域还是局部作用域,省略变量前面的let或者var就会变成一个全局变量。
5.1、验证在块级作用域中
{
var num1 = 678;//全局变量
num2 = 678;//全局变量
let num3 = 678;//局部变量
}
console.log(num1);//678
console.log(num2);//678
console.log(num3);//num3 is not defined
5.2、验证在局部作用域中
function f() {
num1 = 456;//全局变量
var num2 = 678;//局部变量
let num3 = 123;//局部变量
}
f();
console.log(num1);//456
console.log(num2);//num2 is not defined
console.log(num3);//num3 is not defined
❀加餐❀
1. 通过这个🌰 栗子🌰加深一下理解
var num1 = 123;//**全局变量**
function f() {
var num2 = 456//局部变量
}
function test() {
num3 = 666;//局部作用域,没有var或者let修饰。**全局变量**
}
{
var num4 = 789;//块级作用域、**全局变量**
}
{
let num5 = 789;//块级作用域、let定义变量。局部变量
}
{
let value7 = 123;
{
//注意点:在不同的作用域范围中,可以有同名变量
let value7 = 456;//不会报错。
}
}
2. 为什么需要块级作用域
ES5只有全局作用域和函数作用域,没有块级作用域,会带来以下问题:
1) 变量提升导致内层变量可能会覆盖外层变量
var i = 5;
function func() {
console.log(i);
if (true) {
// var i = 6;
}
}
func(); // 5
var i = 5;
function func() {
console.log(i);
if (true) {
let i = 6;
}
}
func(); // 5
var i = 5;
function func() {
console.log(i);
if (true) {
var i = 6;
}
}
func(); // undefined
2) 用来计数的循环变量泄露为全局变量
for (var i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // 10
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // i is not defined
贰 ❀ 变量提升
首先我们要知道,js的执行顺序是由上到下的,但这个顺序,并不完全取决于你,因为js中存在变量的声明提升。
🌰 栗子1
console.log(a) //undefined
var a = 100
fn('zhangsan')
function fn(name){
age = 20
console.log(name, age) //zhangsan 20
}
结果:
打印a的时候,a并没有声明,为什么不报错,而是打印undefined。
执行fn的时候fn并没有声明,为什么fn的语句会执行?
这就是变量的声明提升,代码虽然写成这样,但其实执行顺序是这样的。
var a
function fn(name){
age = 20
console.log(name, age)
}
console.log(a)
a = 100
fn('zhangsan')
js会把所有的声明提到前面,然后再顺序执行赋值等其它操作,因为在打印a之前已经存在a这个变量了,只是没有赋值,所以会打印出undefined,为不是报错,fn同理。
变量提升:JS在解析代码时,会将所有的声明提前到所在作用域的最前面
🌰 栗子2
console.log(name); //undefined
var name = '波妞';
console.log(name); //波妞
function fun(){
console.log(name) //undefined
console.log(like) //undefined
var name = '大西瓜';
var like = '宗介'
}
fun();
相当于
var name;
console.log(name); //undefined
name = '波妞';
console.log(name); //波妞
function fun(){
var name;
var like;
console.log(name) //undefined
console.log(like) //undefined
name = '大西瓜';
like = '宗介'
// 此时再打印
console.log(name) //大西瓜
console.log(like) //宗介
}
fun();
注意:是提前到当前作用域的最前面
这里也要注意函数声明和函数表达式的区别。上例中的fn是函数声明。接下来通过代码区分一下。
🌰 栗子3
// 函数声明
fn1('abc')
function fn1(str){
console.log(str)
}
// 函数表达式
fn2('def')
var fn2 = function(str){
console.log(str)
}
结果:
可以看到fn1被提升了,而fn2的函数体并没有被提升。
效果等同于:
var fn2
fn2('def')
fn2 = function(str){
console.log(str)
}
这下大家应该就明白报错原因了吧!
好了,上面我们已经了解了作用域与变量提升的概念,下面我们来看一下作用域链;
叁 ❀ 作用域链
很简单,直接看🌰 栗子
console.log(name); //undefined (1)
var name = '波妞';
var like = '宗介'
console.log(name); //波妞 (2)
function fun(){
console.log(name); //波妞 (3)
console.log(eat) //ReferenceError: eat is not defined (4)
(function(){
console.log(like) //宗介 (5)
var eat = '肉'
})()
}
fun();
- name定义在全局,在全局可以访问到,所以 (2) 打印能够正确打印;
- 在函数fun中,如果没有定义name属性,那么会到它的父作用域去找,所以 (3) 也能正确打印。
- 定义:内部环境可以访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数。类似单向透明,这就是作用域链,所以 (4) 不行而 (5) 可以。
前后呼应:为什么第一个打印是"undefined",而不是"ReferenceError: name is not defined"。原理就是JS的变量提升
肆 ❀ 闭包
JavaScript高级程序设计中对闭包的定义:闭包是指有权访问另外一个函数作用域中变量的函数。
从概念上,闭包有两个特点:
1、函数嵌套;(有外部函数, 有内部函数)
2、内部函数可以引用外部函数的变量/函数;
以下是我在学习中做的笔记:
1. 🌰引入----代码有点长,但很经典,可以自己运行理解一下哦
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<!--
需求: 点击某个按钮, 提示"点击的是第n个按钮"
-->
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//遍历加监听
/*
(1) 为什么用 length=btns.length?
答:因为btns是一个伪数组,如果直接写 i < btns.length ,每次循环时都要重新统计btns的长度,所以拆分为
length=btns.length; i < length;
(2) 用 var 的效果?
无论我点击哪个按钮,都只是返回 "第4个" 。
将 var 关键字改为 let 关键字就好了;
*/
// 总返回 第4个
/* for (var i = 0,length=btns.length; i < length; i++) {
let btn = btns[i]
btn.onclick = function () {
alert('第'+(i+1)+'个')
}
} */
// 解决方式一:
/* for (var i = 0,length=btns.length; i < length; i++) {
var btn = btns[i]
//将btn所对应的下标保存在btn上
btn.index = i
btn.onclick = function () {
alert('第'+(this.index+1)+'个')
}
} */
// 解决方式二:块级作用域
/* for (let i = 0,length=btns.length; i < length; i++) {
let btn = btns[i]
btn.onclick = function () {
alert('第'+(i+1)+'个')
}
}
*/
// 解决方式三:
//利用闭包
for (var i = 0,length=btns.length; i < length; i++) {
// 自调用函数
(function (j) {
var btn = btns[j]
btn.onclick = function () {
alert('第'+(j+1)+'个')
}
})(i)
}
</script>
</body>
2. 🌰理解闭包
1. 如何产生闭包?
* 当一个被嵌套的内部(子) 函数引用了嵌套其的外部(父) 函数的变量(函数)时, 就产生了闭包
2. 闭包到底是什么?
* 使用chrome调试查看
* 理解一: 闭包是嵌套的内部函数(绝大部分人)
* 理解二: 包含被引用变量(函数)的对象(极少数人)
* 注意: 闭包存在于被嵌套的内部函数中
3. 产生闭包的条件?
* 函数嵌套 (有外部函数,有内部函数)
* 内部函数引用了外部函数的变量/函数
* 执行外部函数;(必须执行外部函数才会产生闭包) ---产生几个闭包要看执行多少次外部函数
🌰栗子
由于种种原因,我们有时候需要得到函数内的局部变量。但是,在正常情况下,这是办不到的,那么怎么才能得到函数内部的局部变量呢?
那就是闭包,在函数的内部,再定义一个函数,然后把这个函数返回。
function F1(){
var a = 100
//返回一个函数 (函数作为返回值)
return function (){
console.log(a)
}
}
//f1得到一个函数
var f1 = F1()
var a = 200
f1() // 100
在本🌰中就实现了闭包,简单的说,闭包就是能够读取其他函数内部变量的函数。
下面解释一下为什么打印的是100,
看这句 var f1 = F1(); F1这个函数执行的结果是返回一个函数,所以就相当于把F1内的函数付给了f1变量,类似于这样:
var f1 = function(){ console.log(a) //这里的a是一个自由变量 }
这里的a是一个自由变量,所以根据作用域链的原理,就应该去上一级作用域去找。之前说过,作用域链在定义时确定,和执行无关,那就去想上找,这个函数定义定义在F1中,所以会在F1中找a这个变量,所以这里会打印的100。
通过这种方式,我们在全局下就读取到了F1函数内部定义的变量,这就是闭包。谈到这里那就说说闭包的作用吧!
3. 🌰闭包的作用
<body>
<!--
作用:
1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中 --- (延长了局部变量的生命周期)
其实这是闭包作用,同时也是闭包的缺点,占用内存,所以说要及时释放闭包(方式: = null)
2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
1. 函数执行完后, 函数内部声明的局部变量是否还存在?
一般情况下是不存在(调用完就清除),;存在闭包时,变量才存在(延长了局部变量的生命周期);
2. 在函数外部能直接访问函数内部的局部变量吗?
不能, 但我们可以通过闭包让外部操作它(栗子在上面)
-->
<script type="text/javascript">
function fn1() {
var a = 2; // 局部变量
function fn3() {
a--;
console.log(a);
}
return fn3
}
var f = fn1()
f() // 1
f() // 0
</script>
</body>
4. 🌰闭包的两种应用场景
<body>
<!--
1. 将函数作为另一个函数的返回值;----函数作为返回值
2. 将函数作为参数传递给另一个函数调用;----函数作为参数
-->
<script type="text/javascript">
// 1. 将函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
// 2. 将函数作为实参传递给另一个函数调用
function F1(){
var a = 100
return function (){
console.log(a)
}
}
var f1 = F1()
function F2(fn){
var a = 200
fn()
}
F2(f1) // 100
</script>
</body>
5. 🌰闭包的生命周期
<body>
<!--
1. 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
2. 死亡: 在嵌套的内部函数成为垃圾对象时
-->
<script type="text/javascript">
function fn1() {
//此时闭包就已经产生了(由于函数提升, 内部函数对象已经创建了)
var a = 2
function fn2 () {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)
</script>
</body>
好了,就到这里,我这只是冰山一角,一点皮毛而已,如果哪里有问题,希望各位大佬指出。DayDayUp!!!