学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。
了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。
局部作用域分为函数作用域和块作用域。
JS 是一种参考其他编程语言后制造出的语言,局部作用域的概念和其他语言 ( 如 C 语言 ) 的思路是差不多的。
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
<script>
// 声明 counter 函数
function counter(x, y) {
// 函数内部声明的变量
const s = x + y
console.log(s) // 18
}
// 设用 counter 函数
counter(10, 8)
// 访问变量 s
console.log(s)// 报错
script>
总结:
在 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
声明成常量。
标签和
.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 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。
在解释什么是作用域链前先来看一段代码:
<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>
从图来看和冒泡事件有点类似
总结:
**垃圾回收机制 **(Garbage Collection) 简称GC
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题
但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况不再用到的内存,没有及时释放,就叫做内存泄漏
内存泄漏: 程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄漏
JS 环境中分配的内存,一般有如下生命周期:
堆栈空间分配区别:
1.栈(操作系统)︰由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
2.堆(操作系统)︰一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。
下面介绍两种常见的浏览器垃圾回收算法:引用计数法和标记清除法
引用计数 (现在基本上不在使用了)
IE采用的引用计数算法,定义 “内存不再使用” ,就是看一个对象是否有指向它的引用,没有引用了就回收对象算法:
跟踪记录被引用的次数
如果被引用了一次,那么就记录次数1,多次引用会累加++
如果减少一个引用就减1–
如果引用次数是0 ,则释放内存
以数组为例
对象的内存回收和数组是一个道理
缺点:
标记清除法 (现在的浏览器 常用)
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
核心:
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
就是从根部( 在JS中就是全局对象 )出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。
那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
标记清除法核心思路:从根部扫描对象,能查找到的就是使用的,查找不到的就要回收
闭包是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数,如下代码所示:
<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
// 注意是 return 的 fn 而不是 fn 加 (), 这样会直接调用 fn
}
const re = outer()
// const re = function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }
re()
re()
// const fn = function() { } 函数表达式
// 4. 闭包存在的问题: 可能会造成内存泄漏
script>
body>
总结:
1.怎么理解闭包?
2.闭包的作用?
3.闭包可能引起的问题?
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问,
<script>
// 访问变量 str
console.log(str + ' world!');
// 声明变量 str
var str = 'hello ';
// 结果为(undefined world)
script>
<script>
// 上面的代码相当于是这样的, 只提升到当前作用域前,而且只提升变量的声明,不提升变量的赋值
var str // 这里的 str 为 undefined
console.log( str +' world')
str = 'hello' // 这里的 str 为 'hello'
script>
总结:
undefined
let
声明的变量不存在变量提升,推荐使用 let
注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let
可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料。
知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
<script>
// 调用函数,在函数声明之前
foo()
// 声明函数
function foo() {
console.log('声明之前即被调用...')
}
script>
<script>
// 上面的函数提升相当于是这样的
function foo() {
console.log('声明之前即被调用...')
}
// 会把所有函数声明提升到当前作用域的最前面
// 只提升函数声明,不提升函数调用
foo()
// 声明函数
function foo() {
console.log('声明之前即被调用...')
}
script>
<script>
// 不存在提升现象
bar() // 错误
var bar = function () {
console.log('函数表达式不存在提升现象...')
}
script>
<script>
bar() // 错误
bar = function () {
console.log('函数表达式不存在提升现象...')
}
script>
<script>
// 上面的代码相当于是这样的情况
var bar // 只提升了变量的声明没有提升函数的声明
bar() // 错误
bar = function () {
console.log('函数表达式不存在提升现象...')
}
script>
函数表达式必须先声明和赋值,后调用否则报错
总结:
函数参数的使用细节,能够提升函数应用的灵活度。
<script>
// 设置参数默认值
function sayHi(name="小明", age=18) {
document.write(`大家好,我叫
${name},我今年${age}岁了。`);
}
// 调用函数
sayHi();
sayHi('小红');
sayHi('小刚', 21);
script>
总结:
undefined
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
的作用是动态获取函数的实参剩余参数允许我们将一个不定数量的参数表示为一个数组
<script>
function config(baseURL, ...other) {
console.log(baseURL) // 得到 'http://baidu.com'
console.log(other) // other 得到 ['get', 'json']
}
// 调用函数
config('http://baidu.com', 'get', 'json');
script>
<script>
function config(baseURL1, baseURL2 ...other) {
console.log(baseURL1) // 得到 'http://baidu.com'
console.log(baseURL2) // 得到 'get'
console.log(other) // other 得到 ['json']
}
// 调用函数
config('http://baidu.com', 'get', 'json');
script>
总结:
...
是语法符号,置于最末函数形参之前,用于获取多余的实参...
获取的剩余实参,是个真数组箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。
使用场景: 箭头函数更适用于那些本来需要匿名函数的地方
<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>
总结:
()
{}
,并自动做为返回值被返回箭头函数中没有 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>
箭头函数不会创建自己的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) // this 指向 obj
let i = 10
const count = () => {
console.log(this) // this 指向 obj
}
count()
}
}
obj.sayHi()
script>
知道解构的语法及分类,使用解构简洁语法快速为变量赋值。
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:
<script>
// 普通的数组
let arr = [1, 2, 3]
// 批量声明变量 a b c
// 同时将数组单元值 1 2 3 依次赋值给变量 a b c
let [a, b, c] = arr
// 或者这样也行
// let [a, b, c] = [1, 2, 3]
// 这样写相当于是
// let a = arr[0]
// let b = arr[1]
// let c = arr[2]
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
script>
有了数组结构后部分操作会变得简单,比如:
let a = 1
let b = 2; // 注意这里的 ; 号必须要加,否则会报错。这里的 ; 号加在 [] 之前也是可以的
[b, a] = [a, b]
console.log(a, b)
立即执行函数必须加分号很好理解。
数组解构必须加分号的理解:
如果不加分号,浏览器会将代码理解成这样,不会帮你自动换行
let a = 1
let b = 2[b, a] = [a,b]
如果加了分号才会避免这样的情况
let a = 1
let b = 2;
[b, a] = [a,b]
;[d, e, f] = [1, 2, 3]
console.log(d)
console.log(e)
console.log(f)
let a = 1
let b = 2;
[b, a] = [a,b]
// 这样不加 ; 号也不会报错
// 上面的写法会让变量变成全局变量
const [d, e, f] = [1, 2, 3]
console.log(d)
console.log(e)
console.log(f)
或者前面没有代码的时候也会避免这样的情况
const [a, b, c] = [1, 2, 3]
console.log(a)
console.log(b)
console.log(c)
数组结构还会可以使用剩余数组:
const [a, b, ...c] = [1,2,3,4]
console.log(a) // 1
console.log(b) // 2
console.log(c) // [3,4]
数组解构还可以使用默认参数
const [a = 0,b = 0] = []
console.1og(a) // 0
console.log(b) // 0
数组解构还可以按需导入赋值
const [a, b, ,d] = [1,2,3,4]
console.log(a) // 1
console.log(b) // 2
console.log(d) // 4
数组解构的小发现
const [a, b, c] = [1, 'a', 3]
console.log(a);
console.log(b);
console.log(c);
// 这里 a 和 c 的数据类型是 number ,但是 b 的数据类型是 string ,和常规的数组是有区别的
// 多维数组解构
const arr = [1,2,[3,4]]
const [a, b, c] = [1,2,[3,4]]
consoLe.Log(a) // 1
console.log(b) // 2
consoLe.Log(c) // [3,4]
// 多维数组解构
const [a, b, [c, d]] = [1, 2, [3,4]]
console. log(a) // 1
console.log(b) // 2
console.log(c) // 3
console.log(d) // 4
总结:
=
左侧的 []
用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量undefined
...
获取剩余单元值,但只能置于最末位undefined
时默认值才会生效注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析
对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:
<script>
// 普通对象
const user = {
name: '小明',
age: 18
};
// 批量声明变量 name age
// 同时将数组单元值 小明 18 依次赋值给变量 name age
const {name, age} = user
// 这里的名称必须和对象里面的名称一样,否则会是 undefined
// 还有外面的变量名不能和解构中的变量名相同否则也会报错,如:const name = 'Tom'
console.log(name) // 小明
console.log(age) // 18
script>
// 由于对象大都是从后台获取的,名称难免会有重复,但是可以通过改名来解决
const uname = 'Tom'
// 改名的方式如下, 冒号的后面是新的名称
const { uname: username, age } = { uname: 'pink老师', age: 18 }
console.log(username)
console.log(age)
总结:
=
左侧的 {}
用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量undefined
undefined
时默认值才会生效注:支持多维解构赋值
// 这是单独一个对象时的解构语法
const pig = {
name:‘佩奇',
family: {
mother: ‘猪妈妈',
father: '猪爸爸',
sister: '乔治'
},
age: 6
}
//多级对象解构
const { name, family: { mother, father, sister }, age } = pig
// 如果这样写会报错
// const { name, { mother, father, sister }, age } = pig
// 这是多个对象成数组时的解构语法
const person = [
name: '佩奇',
family: {
mother:‘猪妈妈',
father: '猪爸爸',
sister: '乔治’
},
age:6
}
]
const [{ name,family: { mother,father, sister }}] = person
console.log(name)
console.log(mother)
console.log(father)
console.log(sister)
<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>
以后从后台获取的数据基本上是这样的,这种情况就需要利用解构来解决了
code 中的 200 表示数据是否请求成功,msg 表示看到底有没有拿到数值,后面的 data 中的内容才是真正的需要的数据
// 可以这样获取数据
const { data } = msg
Console.log(data)
// 一般情况可以这样调用获取的数据
function render(msg) {
const { data } = msg
console.log(data)
}
// 更加简洁可以这样写
// 原理:const { data } = msg 这样表示 { data } 和 msg 是等价的
function render1({ data }) {
console.log(data)
}
// 获取的同时进行改名
function render2({ data: myData }) {
console.log(myData)
}
// 调用函数
render(msg)
render1(msg)
render2(msg)
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数,和 map 很像,区别就是 forEach() 用返回数组,map 要返回。
语法:
被遍历的数组.forEach(function ( 当前数组元素, 当前元素索引号 ) {
//函数体
})
注意:
1.forEach 主要是遍历数组,因为和 for 相比 forEach 更加复杂
2.参数当前数组元素是必须要写的, 索引号可选。
<body>
<script>
// forEach 就是遍历 加强版的for循环 适合于遍历数组对象
const arr = ['red', 'green', 'pink']
arr.forEach(function (item, index) {
console.log(item) // 数组元素 red green pink
console.log(index) // 索引号
})
script>
body>
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>
这一天的案例也是比较经典的,需要重点关注一下,尤其是运用对象解构获取 tagName 和 dataset 的时候。