作用域
很多编程语言就是在变量中存储值,并且能读取和修改此值。
变量该存储在哪,又给如何读取?所以程序制定了一些规则:作用域。
作用域:是可访问变量的集合。
常见的作用域主要分为几个类型:全局作用域(global/window)、函数作用域(function)、块状作用域({})、动态作用域(this)
全局作用域
变量在函数或者代码块 {} 外定义,即为全局作用域。不过,在函数或者代码块 {} 内未定义的变量也是拥有全局作用域的(不推荐)
var name= "es"
// 此处可调用 name变量
function myFunction() {
// 函数内可调用 name变量
}
name拥有作用域,可以在任意地方被读取获取修改
注意
没有使用 var 关键字,该变量依然为全局变量,因为它将作为 global 或者 window 的属性存在。
function myFunction() {
name= "es"
}
函数作用域
在函数内部定义的变量,就是局部作用域。函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域!
function bar() {
var testValue = 'inner'
}
console.log(testValue) // 报错:ReferenceError: testValue is not defined
如果想读取函数内的变量,必须借助 return 或者闭包
闭包,外层作用域想访问函数内部的作用域(变量)。
return 方式外层作用域访问函数内部的作用域
function bar(value) {
var testValue = 'inner'
return testValue + value
}
console.log(bar('fun')) // "innerfun"
闭包方式外层作用域访问函数内部的作用域
function bar(value) {
var testValue = 'inner'
var rusult = testValue + value
function innser() {
return rusult
}
return innser()
}
console.log(bar('fun')) // "innerfun"
return 是函数对外交流的出口,而 return 可以返回的是函数,函数内部的子函数是可以获取函数作用域内的变量的。
问题:如果 inner 函数再嵌套函数呢?
如果 inner 函数再嵌套函数呢?这就涉及到作用域链
上图是作用域链,和原型链相似,任何一个作用域链都是一个堆栈,首先先把全局作用域压入栈底,再按照函数的嵌套关系一次压入堆栈。在执行的时候就按照这个作用域链寻找变量。
堆栈:堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。
原型链图
块状作用域
于什么是块,只要认识 {} 就可以了
if (true) {
let a = 1
console.log(a)
}
if 后 {} 就是“块”,这个里面的变量就是拥有这个块状作用域,按照规则, {} 之外是无法访问这个变量的。
动态作用域
window.a = 3
function test() {
console.log(this.a)
}
test.bind({
a: 2
})() // 2
test() // 3
test.bind已经把作用域的范围进行了修改指向了 { a: 2 }
,而 this 指向的是当前作用域对象。
问题
作用域是在代码编写的时候就已经决定了呢,还是在代码执行的过程中才决定的?
var name= " es"
// 此处可调用 name变量
function myFunc() {
// 函数内可调用 name变量
}
name就是全局作用域,函数内部的用 var 定义的变量就是函数作用域。这个也就是专业术语:词法作用域
变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。
相反,只能在执行阶段才能决定变量的作用域,那就是动态作用域。
新的声明方式
let变量
1. let 声明的全局变量不是全局对象window的属性
let不能通过 window. 变量名 的方式访问这些变量,var 声明的全局变量是 window 的属性,是可以通过 window. 变量名 的方式访问的
var a = 5
console.log(window.a) // 5
let a = 5
console.log(window.a) // undefined
2. 用let定义变量不允许重复声明
使用 var 可以重复定义,使用 let 却不可以
var a = 5
var a = 6
console.log(a) // 5
let a = 5
let a = 6
// VM131:1 Uncaught SyntaxError: Identifier 'a' has already been declared
// at <anonymous>:1:1
3. let声明的变量不存在变量提升
a 的调用在声明之前,所以它的值是 undefined
function foo() {
console.log(a)
var a = 5
}
foo() //undefined
// var 会导致变量提升,上述代码和下面的代码等同
function foo() {
var a
console.log(a)
a = 5
}
foo() //undefined
对于 let 而言,变量的调用是不能先于声明的
function foo() {
console.log(a)
let a = 5
}
foo()
// Uncaught ReferenceError: Cannot access 'a' before initialization
4. let声明的变量具有暂时性死区
只要块级作用域内存在 let 命令,它所声明的变量就绑定在了这个区域,不再受外部的影响。
在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”
var a = 5
if (true) {
a = 6
let a
}
4. let 声明的变量拥有块级作用域
{
let a = 5
}
console.log(a) // undefined
a 变量是在代码块 {} 中使用 let 定义的,它的作用域是这个代码块内部,外部无法访问
for 循环
for (var i = 0 i < 3 i++) {
console.log('循环内:' + i) // 0、1、2
}
console.log('循环外:' + i) // 3
改成let声明
for (let i = 0 i < 3 i++) {
console.log('循环内:' + i) // 0、1、2
}
console.log('循环外:' + i) // ReferenceError: i is not defined
加上setTimeout
https://www.cnblogs.com/planetwithpig/p/12016231.html
babel 网址:https://www.babeljs.cn/repl
function time(){
for(var i=0;i<5;i++){
setTimeout(function(){
console.log('setTimeout的i',i) //5个5
},0)
}
}
time();
1、setTimeOut() 是一个异步函数,JS遇到异步函数的时候,会把异步函数插入到队列中等待。也就是所谓的插队。
2、 流程:for(i=0) ——> for(i=1) ——> for(i=2) for(i=3) ——> for(i=4) ——> for(i=5)ps:(这段循环都内完成了)——>console.log(5) ——> console.log(5) ——> console.log(5) ——> console.log(5) ——> console.log(5) ——> 执行完成
3、 for循环完成之后,i经过5次循环,开始执行setTimeout方法的时候i的值以及变成了5,因为setTimeout有5次方法调用,所以输出5个5
希望的值是0、1、2,.....也就是每次保存住循环时候 i 的值
方案1:闭包
思路:因为setTimeOut()是异步执行,所以让它立即执行就可以了
for(var i=0;i<5;i++){
(function(i){
setTimeout(function(){
console.log('使用闭包的i',i) //0,1,2,3,4
})
})(i)
}
闭包检测到setTimeOut时不再放到队列中进行等待,而是立即运行setTimeOut()
执行流程如下:
for(i=0) ——> console.log(0) ——> for(i=1) ——> console.log(1) ——> for(i=2) ——> console.log(2) ——> for(i=3) ——> console.log(3) ——> for(i=4) ——> console.log(4) ——> for(i=5) ——> 执行结束
内部函数调用外部函数的变量,以确保外部函数的变量不会被释放,内部函数的值引用着外部函数的值
方案2:使用let
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); //0,1,2,3,4
}, 1000 * i);
}
let的作用域是块作用域,所以每次JS检测到setTimeOut,把setTimeOut放到队列的同时,let定义的i的值也会跟随setTimeOut进去队列。所以每次循环后队列里的setTimeOut里的i的值是不一样的
var定义的i是无法进入setTimeOut的。i只能在运行到setTimeOut时才会向外层环境申请i的值,而这个时候i的值已经变成5了。
同步和异步
异步执行原理
https://www.cnblogs.com/lvzl/p/14242510.html
const { log } = console;
log(1); // 首先呢,JS代码是从上至下逐行执行,到这里先打印 1
setTimeout(() => { // 到了这里,遇到了异步任务,把异步操作加到异步队列中,然后接着往下执行JS代码
log(2);
});
new Promise((resolve, reject) => {
log(3); // 执行到这里,这里的代码也是同步的,因此打印 3
resolve(); // resolve 执行以后会进入.then, .then里面也是异步执行, 因此加入异步队列,整个的JS代码第一次就执行完了
}).then(() => {
log(4);
});
// 现在异步队列中有两个任务, setTimeout,Promise.then. JS在执行下一个宏任务之前会保证微任务队列为空,因此会先打印 4, 再打印 3
// 微任务: Promise.then, process.nextTick(node)
// 宏任务: 整体的JS代码, setTimeout, setInterval
// 正确答案: 1324
Const
不能被改变的叫做常量
const 除了具有 let 的块级作用域和不会变量提升外,还有就是它定义的是常量,在用 const 定义变量后,我们就不能修改它了,对变量的修改会抛出异常
const PI = 3.1415
console.log(PI)
PI = 5
console.log(PI)
// Uncaught TypeError: Assignment to constant variable.
这个代码块中因为对 PI 尝试修改,导致浏览器报错,这就说明 const 定义的变量是不能被修改的,它是只读的。
重点
const obj = {
name: 'xiaozhang',
age: 34
}
obj.name= 'xiaowu'
console.log(obj)
// {name: "xiaowu", age: 34"}
const 定义的 obj 竟然被改变了...
这个时候就要去了解js的变量是如何存储的
栈: 原始数据类型(Undefined,Null,Boolean,Number、String)
堆: 引用数据类型(对象、数组、函数)
基本数据类型存储在 栈内存 中,引用数据类型存储在 堆内存 中然后在栈内存中保存 引用地址
const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
如何让对象或者数组这种引用数据类型也不被改变呢?
Object.freeze(obj)
Object.freeze() 只是浅层冻结,只会对最近一层的对象进行冻结,并不会对深层对象冻结。