JavaScript 进阶 - 第1天
学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。
- 理解作用域对程序执行的影响
- 能够分析程序执行的作用域范围
- 理解闭包本质,利用闭包创建隔离作用域
- 了解什么变量提升及函数提升
- 掌握箭头函数、解析剩余参数等简洁语法
1 作用域
了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。
1.1 局部作用域
局部作用域分为函数作用域和块作用域。
1.1.1 函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
<script> // 声明 counter 函数 function counter(x, y) { // 函数内部声明的变量 const s = x + y console.log(s) // 18 } // 设用 counter 函数 counter(10, 8) // 访问变量 s console.log(s)// 报错 </script>
总结:
- 函数内部声明的变量,在函数外部无法被访问。
- 函数的参数也是函数内部的局部变量。
- 不同函数内部声明的变量无法互相访问。
- 函数执行完毕后,函数内部的变量实际被清空了。
1.1.2 块作用域
在 JavaScript 中使用
{}
包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。<script> { // age 只能在该代码块中被访问 let age = 18; console.log(age); // 正常 } // 超出了 age 的作用域 console.log(age) // 报错 let flag = true; if(flag) { // str 只能在该代码块中被访问 let str = 'hello world!' console.log(str); // 正常 } // 超出了 age 的作用域 console.log(str); // 报错 for(let t = 1; t <= 6; t++) { // t 只能在该代码块中被访问 console.log(t); // 正常 } // 超出了 t 的作用域 console.log(t); // 报错 </script>
JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。
<script> // 必须要有值 const version = '1.0.0'; // 不能重新赋值 // version = '1.0.1'; // 常量值为对象类型 const user = { name: '小明', age: 18 } // 不能重新赋值 user = {}; // 属性和方法允许被修改 user.name = '小小明'; user.gender = '男'; </script>
总结:
let
声明的变量会产生块作用域,var
不会产生块作用域。
const
声明的常量也会产生块作用域。
- 不同代码块之间的变量无法互相访问。
- 推荐使用
let
或const
。
注:开发中
let
和 const
经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const
声明成常量。1.2 全局作用域
<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.3 作用域链
在解释什么是作用域链前先来看一段代码:
<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.4 垃圾回收机制
内存的生命周期
// 为变量分配内存 const age = 18 // 为对象分配内存 const obj = { age: 19 } // 为函数分配内存 function fn() { const age = 18 console.log(age) }
JS环境中分配的内存,一般有如下生命周期:
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存。
- 内存使用:即读写内存,也就是使用变量、函数等。
- 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存。
说明:
- 全局变量一般不会回收(关闭页面回收)。
- 一般情况下局部变量的值,不用了,会被自动回收掉。
内存泄漏:程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄露。
1.4.1 拓展-JS垃圾回收机制-算法说明
堆栈空间分配区别:
- 栈(操作系统):由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
- 堆(操作系统):一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。
瞎买美女介绍两种常见的浏览器垃圾回收算法;
引用计数法
和标记清除法
。引用计数法
IE 采用的引用技术算法,定义”内存不再使用“,就是看一个对象是否有指向它的引用,没有引用了就回收对象。
算法:
- 跟踪记录被引用的次数。
- 如果被引用一次,那么就记录次数1,多次引用会
累加 ++
- 如果减少一个引用就
减 --
- 如果引用次数是0,则释放内存
cosnt arr = [1, 2, 3, 4] arr = null
let person = { age: 18, name: '尘缘' } let p = person person = 1 p = null
由上面可以看出,引用计数算法是个简答有效的算法。
引用计数存在一个致命的问题:嵌套引用(循环引用)
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
function fn() { let o1 = {} let o2 = {} o1.a = o2 o2.a = o1 return '引用计数无法回收' } fn()
因为他们的用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露。
标记清除法
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
核心:
- 标记清除算法将”不再使用的对象“定义为”无法达到的对象“。
- 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。
- 那些无法由根部出发触及的对象被标记为不再使用,稍后进行回收。
1.5 闭包
闭包是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数,如下代码所示:
<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}次`) // } // 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.闭包可能引起的问题?
- 内存泄漏
1.6 变量提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)。
<script> // 访问变量 str console.log(str + 'world!'); // 声明变量 str var str = 'hello '; </script>
总结:
- 变量在未声明即被访问时会报语法错误
- 变量在声明之前即被访问,变量的值为
undefined
let/const
声明的变量不存在变量提升,推荐使用let
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用
let
可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料。2 函数
知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。
2.1 函数进阶(提升)
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
<script> // 调用函数 foo() // 声明函数 function foo() { console.log('声明之前即被调用...') } // 不存在提升现象 bar() // 错误 var bar = function () { console.log('函数表达式不存在提升现象...') } </script>
总结:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
2.2 函数参数
函数参数的使用细节,能够提升函数应用的灵活度。
2.2.1 默认值
<script> // 设置参数默认值 function sayHi(name="小明", age=18) { document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`); } // 调用函数 sayHi(); sayHi('小红'); sayHi('小刚', 21); </script>
总结:
- 声明函数时为形参赋值即为参数的默认值
- 如果参数未自定义默认值时,参数的默认值为
undefined
- 调用函数时没有传入对应实参时,参数的默认值被当做实参传入
2.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
的作用是动态获取函数的实参。
- 可以通过for循环依次得到传递过来的实参。
2.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>
总结:
...
是语法符号,置于最末函数形参之前,用于获取多余的实参。
- 借助
...
获取的剩余实参,是个真数组。
2.2.4 展开运算符
目标:能够使用展开运算符并说出常用的使用场景。
展开运算符(...),将一个数组进行展开。
const arr = [1, 5, 3, 8, 2] const.log(...arr) // 1 5 3 8 2
说明:
- 不会修改原数组。
求数组最大值(最小值)。
const arr = [1, 5, 3, 8, 2] // console.log(...arr) // 1 5 3 8 2 console.log(Math.max(...arr)) // 8 console.log(Math.min(...arr)) // 1
合并数组。
// 合并数组 const arr1 = [1, 2, 3] const arr2 = [4, 5, 6] const arr3 = [...arr1, ...arr2] console.log(arr3) // [1, 2, 3, 4, 5, 6]
展开运算符 or 剩余参数
剩余参数:函数参数使用,得到真数组。
展开运算符:数组中使用,数组展开。
function getSum(...other) { // other 得到 [1,2,3] console.log(other) } getSum(1, 2, 3)
const arr = [1, 5, 3, 8, 2] const.log(...arr) // 1 5 3 8 2
2.3 箭头函数(重要)
箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。
<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>
总结:
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号
()
- 箭头函数函数体只有一行代码时可以省略花括号
{}
,并自动做为返回值被返回
2.3.1 箭头函数参数
- 普通函数有
arguments
动态参数
- 箭头函数没有
arguments
动态参数,但是有 剩余参数 ...args
箭头函数中没有
arguments
,只能使用 ...
动态获取实参<body> <script> // 1. 利用箭头函数来求和 const getSum = (...arr) => { let sum = 0 for (let i = 0; i < arr.length; i++) { sum += arr[i] } return sum // 注意函数体有多行代码需要return } const result = getSum(2, 3, 4) console.log(result) // 9 </script>
2.3.2 箭头函数 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: '尘缘', // sayHi: () => { // console.log(this) // this 指向谁? window // } // } // obj.sayHi() const obj = { uname: '尘缘', sayHi: function () { console.log(this) // obj let i = 10 const count = () => { console.log(this) // obj } count() } } obj.sayHi() </script>
在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此 DOM事件回调函数为了简便,还是不太推荐使用箭头函数。
// DOM 节点 const btn = document.querySelector('.btn') // 箭头函数 此时 this 指向了 window btn.addEventListener('click', () => { console.log(this) }) // 普通函数 此时 this 指向了 DOM 对象 btn.addEventListener('click', function () { console.log(this) })
3 解构赋值
知道解构的语法及分类,使用解构简洁语法快速为变量赋值。
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。
3.1 数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:
<script> // 普通的数组 let arr = [1, 2, 3] // 批量声明变量 a b c // 同时将数组单元值 1 2 3 依次赋值给变量 a b c let [a, b, c] = arr console.log(a); // 1 console.log(b); // 2 console.log(c); // 3 </script>
总结:
- 赋值运算符
=
左侧的[]
用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
- 变量的顺序对应数组单元值的位置依次进行赋值操作
- 变量的数量大于单元值数量时,多余的变量将被赋值为
undefined
- 变量的数量小于单元值数量时,可以通过
...
获取剩余单元值,但只能置于最末位
- 允许初始化变量的默认值,且只有单元值为
undefined
时默认值才会生效
注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析
3.2 对象解构
对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:
<script> // 普通对象 const user = { name: '小明', age: 18 }; // 批量声明变量 name age // 同时将数组单元值 小明 18 依次赋值给变量 name age const {name, age} = user console.log(name) // 小明 console.log(age) // 18 </script>
总结:
- 赋值运算符
=
左侧的{}
用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
- 对象属性的值将被赋值给与属性名相同的变量
- 对象中找不到与变量名一致的属性时变量值为
undefined
- 允许初始化变量的默认值,属性不存在或单元值为
undefined
时默认值才会生效
注:支持多维解构赋值
<body> <script> // 1. 这是后台传递过来的数据 const msg = { "code": 200, "msg": "获取新闻列表成功", "data": [ { "id": 1, "title": "5G商用自己,三大运用商收入下降", "count": 58 }, { "id": 2, "title": "国际媒体头条速览", "count": 56 }, { "id": 3, "title": "乌克兰和俄罗斯持续冲突", "count": 1669 }, ] } // 需求1: 请将以上msg对象 采用对象解构的方式 只选出 data 方面后面使用渲染页面 // const { data } = msg // console.log(data) // 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数 // const { data } = msg // msg 虽然很多属性,但是我们利用解构只要 data值 function render({ data }) { // const { data } = arr // 我们只要 data 数据 // 内部处理 console.log(data) } render(msg) // 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData function render({ data: myData }) { // 要求将 获取过来的 data数据 更名为 myData // 内部处理 console.log(myData) } render(msg) </script>
4 综合案例
4.1 forEach遍历数组(重点)
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。
注意:1.forEach 主要是遍历数组2.参数当前数组元素是必须要写的, 索引号可选。
<body> <script> // forEach 就是遍历 加强版的for循环 适合于遍历数组对象 const arr = ['red', 'green', 'pink'] const result = arr.forEach(function (item, index) { console.log(item) // 数组元素 red green pink console.log(index) // 索引号 }) // console.log(result) </script> </body>
4.2 filter筛选数组(重点)
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组。
<body> <script> const arr = [10, 20, 30] // const newArr = arr.filter(function (item, index) { // // console.log(item) // // console.log(index) // return item >= 20 // }) // 返回的符合条件的新数组 const newArr = arr.filter(item => item >= 20) console.log(newArr) </script> </body>
致谢:
有关Notion安装或者使用上的问题,欢迎您在底部评论区留言,一起交流~