参考视频——黑马程序员
JavaScript 进阶 - 第 1 天
学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。
- 理解作用域对程序执行的影响
- 能够分析程序执行的作用域范围
- 理解闭包本质,利用闭包创建隔离作用域
- 了解什么变量提升及函数提升
- 掌握箭头函数、解析剩余参数等简洁语法
一、作用域
了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。
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>
总结:
- 函数内部声明的变量,在函数外部无法被访问
- 函数的参数也是函数内部的局部变量
- 不同函数内部声明的变量无法互相访问
- 函数执行完毕后,函数内部的变量实际被清空了
块作用域
在 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 垃圾回收机制
- 引用计数
- 标记清除法(常用
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>
const fun1 = () => {
const a = 1;
return () => {
console.log("a", a);
};
};
const fun = fun1();
fun();
总结:
- 怎么理解闭包?
- 闭包 = 内层函数 + 外层函数的变量
- 闭包的作用?
- 封闭数据,实现数据私有,外部也可以访问函数内部的变量
- 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来
- 闭包可能引起的问题?
- 内存泄漏(没有被回收),因为外层函数 return 内层函数,又被全局作用域使用,而全局作用域暂时不会被销毁;再根据标记消除法,一直能找到 fn,fn 找到 i,所以会泄露
变量提升
变量提升是 JavaScript 中比较“奇怪”的现象(是一种缺陷),它允许在变量声明之前即被访问,仅存在于var声明变量
注意:
- 在代码在执行之前,首先会把所有在当前作用域下,所有var声明的变量提升到当前作用域的最前面
- 只提升声明,不提升赋值,
例如:
// 初始代码:
console.log("num是:", num);
var num = 10;
// 提升后 :
var num;
console.log("num是:", num);
num = 10;
<script>
// 访问变量 str
console.log(str + "world!");
// 声明变量 str
var str = "hello ";
</script>
因为不提升赋值,所以打印出来的是undefined
例子2:
提升后:
总结:
- 变量在未声明即被访问时会报语法错误
- 变量在声明之前即被访问,变量的值为
undefined
let
声明的变量不存在变量提升,推荐使用let
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let
可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料。
二、函数
知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。
2.1 函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
注意:
- 会把所有函数声明提升到当前作用域的最前面
- 只提升函数声明,不提升函数调用
<script>
// 调用函数
foo();
// 声明函数
function foo() {
console.log("声明之前即被调用...");
}
// 不存在提升现象
bar(); // 错误
var bar = function () { // 函数表达式
console.log("函数表达式不存在提升现象...");
};
// 相当于:
var bar;
bar();
bar = function () { // 函数表达式
console.log("函数表达式不存在提升现象...");
};
</script>
总结:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
2.2 函数参数
函数参数的使用细节,能够提升函数应用的灵活度。
还有形参
和实参
,不再赘述
默认值
<script>
// 设置参数默认值
function sayHi(name = "小明", age = 18) {
document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
}
// 调用函数
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>
function fun() {
console.log("argument", arguments);
let sum = 0;
// arguments 对象并不是一个真正的数组,
// 它是一个类似数组的对象,但没有数组的方法,比如 map
let argsArray = Array.from(arguments); // 将 arguments 转换为数组
argsArray.map((item) => {
sum += item;
});
console.log("sum", sum);
}
fun(2, 2, 3, 6, 9, 8);
或者:
function fun() {
console.log("argument", arguments);
// arguments 对象并不是一个真正的数组,
// 它是一个类似数组的对象,但没有数组的方法,比如 map
let argsArray = Array.from(arguments); // 将 arguments 转换为数组
const result = argsArray.reduce((sum, item) => {
return (sum += item);
}, 0);
console.log("result", result);
}
fun(2, 2, 3, 6, 9, 8);
总结:
arguments
是一个伪数组(不能使用push
、map
等方法),只存在函数中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>
总结:
...
是语法符号,置于最末函数形参之前,用于获取多余的实参- 借助
...
获取的剩余实参,是个真数组
补充:展开运算符 …arr
上面的剩余参数和展开运算符是2个东西,不是同一个概念
使用场景1:合并对象
const obj1 = {
name: "slx",
age: 11,
};
const obj2 = {
clsss: "jk",
sex: "f",
name: "dddd",
};
const obj3 = { ...obj1, ...obj2 };
console.log("obj3", obj3);
使用场景2:合并数组
const arr1 = [1, 2, 3, 4];
const arr2 = [5, 6, 7];
console.log([...arr1, ...arr2]);
使用场景3: 数组求最大值
const arr = [1, 2, 3, 4];
console.log(Math.max(...arr)); // 也就是...arr ==== 1,2,3,4
// 错误写法:
console.log(Math.max(arr)); // 不能直接对数组求最大值
补充:匿名函数
在JavaScript中,匿名函数是一个没有名字的函数表达式,它通常用于立即调用或作为参数传递给其他函数。匿名函数是非常灵活的,可以在多种情况下使用。
匿名函数主要有两种形式:函数表达式和函数声明。
- 函数表达式(Function Expression)
函数表达式是一种匿名函数的写法,它使用大括号{}
来定义函数体,并且通过变量名来引用函数。例如:
const myFunction = function() {
console.log('这是一个匿名函数!');
};
myFunction(); // 调用匿名函数
在这个例子中,myFunction
是一个变量,它引用了匿名函数。你可以给这个匿名函数赋值给任何变量,包括匿名函数的返回值。
2. 箭头函数(Arrow Function)
箭头函数是ES6引入的一种新的函数语法,它提供了一种更简洁的写法。箭头函数通常用于匿名函数,并且有一些不同于传统函数的表达式。例如:
const myArrowFunction = () => {
console.log('这是一个箭头函数!');
};
myArrowFunction(); // 调用箭头函数
- 作为回调函数传递给其他函数,例如事件处理函数或异步操作的回调:
document.querySelector('#myButton').addEventListener('click', function() {
console.log('按钮被点击!');
});
- 立即调用一个函数,通常用于封装一些代码并在创建函数时就执行它:
(function() {
console.log('这是一个立即调用的匿名函数!');
})();
匿名函数是JavaScript中非常强大的特性之一,它们在异步编程和函数式编程中扮演着重要角色。
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>
总结:
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号
()
- 箭头函数函数体只有一行代码时可以省略花括号
{}
,并自动做为返回值被返回
箭头函数参数
箭头函数中没有 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>
</body>
箭头函数 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>
三、解构赋值
知道解构的语法及分类,使用解构简洁语法快速为变量赋值。
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。
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
时默认值才会生效
6. 按需导入,忽略某些返回值
7. 多维数组的结构
注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析
补充:js前面必须加分号的两种情况
-
立即执行函数
-
数组解构
-
[1,2,3].map
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>
2. 对象解构的变量名可以重新改名
旧变量名:新变量名
const obj1 = {
name111: "slx",
age1111: 12,
};
const { name111: name, age111: age } = obj1;
console.log("name", name);
console.log("age", age);
- 数组对象解构
4. 多级解构
总结:
- 赋值运算符
=
左侧的{}
用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量 - 对象属性的值将被赋值给与属性名相同的变量
- 对象中找不到与变量名一致的属性时变量值为
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>
</body>
综合案例
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>
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>