接上篇博客进行学习,通俗易懂,详细
博客地址: 2023年web前端开发之JavaScript基础(五)基础完结_努力的小周同学的博客-CSDN博客
学习内容
学习 作用域、变量提升、 闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法, 降低代码的冗余度。
理解作用域对程序执行的影响
能够分析程序执行的作用域范围
理解闭包本质,利用闭包创建隔离作用域 (重要)
了解什么变量提升及函数提升
掌握箭头函数、解析剩余参数等简洁语法 ( 重点 )
一.作用域
了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。
作用域(scope) 规定了变量能够被访问的"范围" 离开这个"范围" 变量便不能被访问
作用域分为 全局作用域 和 局部作用域.
1.1局部作用域
局部作用域分为函数作用域和块作用域.
1.2函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问
<!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>
//声明conunter 函数
function conunter(x,y){
//函数内部声明的变量
const s = x + y
console.log(s) // 输出18
}
//调用counter 函数
conunter(10,8) // 传入实参
//直接在外部访问 函数内部的变量s
console.log(s) // 报错
</script>
</body>
</html>
总结:
函数内部声明的变量,在函数外部无法被访问
函数的参数也是函数内部的局部变量
不同函数内部声明的变量无法互相访问
函数执行完毕后,函数内部的变量实际被清空了
1.3 块作用域
在 JavaScript 中使用 {} 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。
代码实现效果如下:
<!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>
//代码块
{
//内部声明变量
let age = 12
console.log(age) //12
}
//代码块外部使用 变量age
console.log(age) // 报错
let flag = true
if(flag){
//str 只能在该代码块中被访问
let str = "代码块内部"
console.log(str) // 正常输出 代码块内部
}
//外部访问str 变量
console.log(str) //报错
for(let t = 1; t<=6;t++){
//t 只能在该代码块中被访问
console.log(t); //正常输出
}
//变量t 定义在for循环里
//在外部访问 t
console.log(t) //报错
</script>
</body>
</html>
JavaScript中除了变量外还有产量, 常量与变量本质的区别是 [常量必须要有值且不允许被重新赋值 ], 常量值为对象尤其属性和方法允许重新赋值
<script>
// 设置常量 必须要有值
const version = '1.0.0';
// 常量 不能重新赋值
// version = '1.0.1';
// 常量值为对象类型
const user = {
name: '小明',
age: 18
}
// 不能重新赋值
user = {};
// 属性和方法允许被修改
user.name = '小小明';
user.gender = '男';
</script>
总结:
let 声明的变量会产生块作用域, var 不会产生块作用域 当var设置变量外部也能访问
const 声明的常量也会产生块作用域
不同代码块之间的变量无法互相访问
推荐使用 let 或 const
注:开发中 let 和 const 经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const 声明成常量。
1.4 全局作用域
<script> 标签和 .js 文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
<script>
// 此处是全局
function sayHi() {
// 此处为局部
}
// 此处为全局
</script>
全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:
<script>
// 全局变量 name
const name = '小明'
// 函数作用域中访问全局
function sayHi() {
// 此处为局部
console.log('你好' + name)
}
// 全局变量 flag 和 x
const flag = true
let x = 10
// 块作用域中访问全局
if(flag) {
let y = 5
console.log(x + y) // x 是全局的
}
</script>
总结:
为 window 对象动态添加的属性默认也是全局的,不推荐!
函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
尽可能少的声明全局变量,防止全局变量被污染
JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。
1.5 作用域链
在解释什么是作用域链前先来看一段代码:
<script>
// 全局作用域
let a = 1
let b = 2
// 局部作用域
function f() {
let c
// 局部作用域
function g() {
let d = 'yo'
}
}
</script>
函数内部允许创建新的函数,f 函数内部创建的新函数 g,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。
如下图所示,父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。
作用域链本质上是底层的变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:
这里多理解实验
<script>
// 全局作用域
let a = 1
let b = 2
// 局部作用域
function f() {
let c
// let a = 10;
console.log(a) // 1 或 10
console.log(d) // 报错
// 局部作用域
function g() {
let d = 'yo'
// let b = 20;
console.log(b) // 2 或 20
}
// 调用 g 函数
g()
}
console.log(c) // 报错
console.log(d) // 报错
f();
</script>
总结:
嵌套关系的作用域串联起来形成了作用域链
相同作用域链中按着从小到大的规则查找变量
子作用域能够访问父作用域,父级作用域无法访问子级作用域
思考:
1. 作用域链本质是什么? Ø 作用域链本质上是底层的变量查找机制 2. 作用域链查找的规则是什么? Ø 会优先查找当前函数作用域中查找变量 Ø 查找不到则会依次逐级查找父级作用域直到全局作用域
二. JS垃圾回收机制(重点理解)
2.1 什么是垃圾回收机制
垃圾回收机制(Garbage Collection) 简称 GC
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收.
正因为垃圾回收器的村寨,许多人认为JS不用太关心内存管理问题
但如果不了解JS内存管理机制,我们同样容易层内存泄漏(内存无法被回收) 的情况
不在用到的内存,没有及时释放,就叫做内存泄漏
2.2内存的生命周期
JS环境中分配的内存,一般有如下生命周期:
内存分配:当我们声明变量, 函数, 对象的时候,系统会自动为他们分配内存
内存使用: 及读写内存,也就是使用变量,函数等
内存回收: 使用完毕,由垃圾回收自动回收不再使用的内存
说明:
全局变量一般不会回收(关闭页面回收)
一般情况下局部变量的值不用了会被自动回收掉
总结:
1. 什么是垃圾回收机制? Ø 简称 GC Ø JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾 回收器自动回收
2. 什么是内存泄漏? Ø 不再用到的内存,没有及时释放,就叫做内存泄漏
3. 内存的生命周期是什么样的? Ø 内存分配、内存使用、内存回收 Ø 全局变量一般不会回收; 一般情况下局部变量的值, 不用了, 会被自动 回收掉
拓展-JS垃圾回收机制-算法说明:
堆栈空间分配区别:
1.1 栈 (操作系统) : 由操作系统自动分配释放函数的参数值,局部变量等,基本数据类型放到栈里面
1.2 堆 (操作系统) : 一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收. 复杂数据类型放到堆里面.
下面介绍两种常见的浏览器垃圾回收算法: 引用计数法 和 标记清除法
1.1引用计数法:
IE采用的引用计数算法, 定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象 算法: 1. 跟踪记录被引用的次数 2. 如果被引用了一次,那么就记录次数1,多次引用会累加 ++ 3. 如果减少一个引用就减1 -- 4. 如果引用次数是0 ,则释放内存
let a = new Object() // 引用次数初始化为1
let b = a // 引用次数2,即obj被a和b引用
a=null // 引用次数1
b=null // 引用次数0,
... // GC回收此引用类型在堆空间中所占的内存
但是这种方法也存在一些问题,就是嵌套引用(循环引用)
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄漏
function fn(){ // fn引用次数为1,因为window.fn = fn,会在window=null即浏览器关闭时回收
let A = new Object() // A: 1
let B = new Object() // B: 1
A.b = B // B: 2
B.a = A // A: 2
}
// A对象中引用了B,B对象中引用了A,两者引用计数都不为0,永远不会被回收。
// 若执行无限多次fn,那么内存将会被占满,程序宕机
fn();
// 还有就是这种方法需要一个计数器,这个计数器可能要占据很大的位置,因为我们无法知道被引用数量多少。
因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露
1.2 标记清除法
该策略分为Mark(标记)和Sweep(清除)两个阶段。
Mark阶段:
运行时,将内存中的所有变量标记为0(垃圾)
从各个根对象遍历,将非垃圾变量标记为1
Sweep阶段:
将所有标记为0的变量的内存释放
function fn(a){ // 开始执行此函数时,将其作用域中a、B以及匿名函数标记为0
alert(a) // 0
let B = new Object() // 0
return function (){ // 由于这里return出去会被其他变量引用,故标记变为1
altert(B) // 由于这里的闭包,B的标记变为1
}
... // 执行函数完毕,销毁作用域,在某个GC回收循环时会清理标记为0的变量a,B和匿名函数被保留了下来即非垃圾变量
}
let fn2 = fn(new Object())
// 补充一下:fn和fn2作为window.fn和window.fn2,标记一直为1,仅仅当手动设置fn=null和fn2=null才会标记为0
三. 闭包(重要)
闭包是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数,如下代码所示:
闭包含义 : 内层函数 + 外层函数变量
闭包的应用: 实现数据的私有。统计函数的调用次数
闭包的写法 统计函数的调用次数
<body>
<script>
// 1. 闭包含义 : 内层函数 + 外层函数变量
// function outer() {
// const a = 1 //外层函数变量
//
// function f() { //内层函数
// console.log(a)
// }
//
// f()
//
// }
// outer()
// 2. 闭包的应用: 实现数据的私有.统计函数的调用次数
// let count = 1
// function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }
// 不影响外部count = 1 的值,实现数据的私有
// 3. 闭包的写法 统计函数的调用次数
function outer() {
let count = 1
function fn() {
count++
console.log(`函数被调用${count}次`)
}
return fn
}
const re = outer()
// const re = function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }
re()
re()
// const fn = function() { } 函数表达式
// 4. 闭包存在的问题: 可能会造成内存泄漏
</script>
</body>
总结:
1.怎么理解闭包?
闭包 = 内层函数 + 外层函数的变量
恍然大悟如下:
2.闭包的作用?
封闭数据,提供操作,外部也可以访问函数内部的变量
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来, 实现数据私有
3.闭包可能引起的问题?
内存泄漏
四. 变量提升
目标:了解什么是变量提升 说明:
JS初学者经常花很多时间才能习惯变量提升,还经常出现一些意想不到的bug,正因为如此,ES6 引入了块级作用域, 用let 或者 const声明变量,让代码写法更加规范和人性化
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问,(仅存在于var声明变量)
<script>
// 访问变量 str
console.log(str + 'world!');
// 声明变量 str
var str = 'hello ';
</script>
注意: 1. 变量在未声明即被访问时会报语法错误 2. 变量在var声明之前即被访问,变量的值为 undefined 3. let / const 声明的变量不存在变量提升 4. 变量提升出现在相同作用域当中 5. 实际开发中推荐先声明再访问变量
提问:
1. 用哪个关键字声明变量会有变量提升?
var
2. 变量提升是什么流程?
先把var 变量提升到当前作用域于最前面
只提升变量声明, 不提升变量赋值
然后依次执行代码
我们不建议使用var声明变量
五.函数进阶
知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异
1.函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用
<script>
// 调用函数
foo()
// 声明函数
function foo() {
console.log('声明之前即被调用...')
}
// 不存在提升现象
bar() // 错误
var bar = function () {
console.log('函数表达式不存在提升现象...')
}
</script>
总结: 1. 函数提升能够使函数的声明调用更灵活 2. 函数表达式不存在提升的现象 3. 函数提升出现在相同作用域当中
2.函数参数
函数参数的使用细节,能够提升函数应用的灵活度。
2.1默认值
<script>
// 设置参数默认值
function sayHi(name="小明", age=18) {
document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
}
// 调用函数
sayHi();
sayHi('小红');
sayHi('小刚', 21);
</script>
总结:
声明函数时为形参赋值即为参数的默认值
如果参数未自定义默认值时,参数的默认值为 undefined
调用函数时没有传入对应实参时,参数的默认值被当做实参传入
2.2动态参数
arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。
<script>
// 求生函数,计算所有参数的和
function sum() {
// console.log(arguments)
let s = 0
for(let i = 0; i < arguments.length; i++) {
s += arguments[i]
}
console.log(s)
}
// 调用求和函数
sum(5, 10)// 两个参数
sum(1, 2, 4) // 三个参数
</script>
总结:
arguments 是一个伪数组
arguments 的作用是动态获取函数的实参
2.3 剩余参数
<script>
function config(baseURL, ...other) {
console.log(baseURL) // 得到 'http://baidu.com'
console.log(other) // other 得到 ['get', 'json']
}
// 调用函数
config('http://baidu.com', 'get', 'json');
</script>
总结:
... 是语法符号,置于最末函数形参之前,用于获取多余的实参
借助 ... 获取的剩余实参,是个真数组
3. 展开运算符
展开运算符( … ),将一个数组进行展开 典型运用场景: 求数组最大值(最小值)、合并数组等
展开运算符 or 剩余参数
剩余参数:函数参数使用,得到真数组 展开运算符:数组中使用,数组展开
3.箭头函数( 重点 )
箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。
3.1基本写法
<body>
<script>
// const fn = function () {
// console.log(123)
// }
// 1. 箭头函数 基本语法
// const fn = () => {
// console.log(123)
// }
// fn()
// const fn = (x) => {
// console.log(x)
// }
// fn(1)
// 2. 只有一个形参的时候,可以省略小括号
// const fn = x => {
// console.log(x)
// }
// fn(1)
// // 3. 只有一行代码的时候,我们可以省略大括号
// const fn = x => console.log(x)
// fn(1)
// 4. 只有一行代码的时候,可以省略return
// const fn = x => x + x
// console.log(fn(1))
// 5. 箭头函数可以直接返回一个对象
// const fn = (uname) => ({ uname: uname })
// console.log(fn('刘德华'))
</script>
</body>
总结:
箭头函数属于表达式函数,因此不存在函数提升
箭头函数只有一个参数时可以省略圆括号 ( )
箭头函数函数体只有一行代码时可以省略花括号{ }, 并自动作为返回值被返回
如图所示:
代码如下:
<body>
<script>
// const fn = function () {
// console.log(123)
// }
// 1. 箭头函数 基本语法
// const fn = () => {
// console.log(123)
// }
// fn()
// const fn = (x) => {
// console.log(x)
// }
// fn(1)
// 2. 只有一个形参的时候,可以省略小括号
// const fn = x => {
// console.log(x)
// }
// fn(1)
// // 3. 只有一行代码的时候,我们可以省略大括号
// const fn = x => console.log(x)
// fn(1)
// 4. 只有一行代码的时候,可以省略return
// const fn = x => x + x
// console.log(fn(1))
// 5. 箭头函数可以直接返回一个对象
// const fn = (uname) => ({ uname: uname })
// console.log(fn('刘德华'))
</script>
</body>
3.2箭头函数参数
箭头函数中没有 arguments,只能使用 ... 动态获取实参
<body>
<script>
// 1. 利用箭头函数来求和
const getSum = (...arr) => {
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum
}
const result = getSum(2, 3, 4)
console.log(result) // 9
</script>
3.3箭头函数 this
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。
<script>
// 以前this的指向: 谁调用的这个函数,this 就指向谁
// console.log(this) // window
// // 普通函数
// function fn() {
// console.log(this) // window
// }
// window.fn()
// // 对象方法里面的this
// const obj = {
// name: 'andy',
// sayHi: function () {
// console.log(this) // obj
// }
// }
// obj.sayHi()
// 2. 箭头函数的this 是上一层作用域的this 指向
// const fn = () => {
// console.log(this) // window
// }
// fn()
// 对象方法箭头函数 this
// const obj = {
// uname: 'pink老师',
// sayHi: () => {
// console.log(this) // this 指向谁? window
// }
// }
// obj.sayHi()
const obj = {
uname: 'pink老师',
sayHi: function () {
console.log(this) // obj
let i = 10
const count = () => {
console.log(this) // obj
}
count()
}
}
obj.sayHi()
</script>
箭头函数不会创建自己的this ,他只会从自己的作用域链的上一层沿用this