1、作用域
1.1、作用域
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问
作用域分为:
- 局部作用域
- 全局作用域
局部作用域
局部作用域又分为函数作用域和块作用域
函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问
- 函数的参数也是函数内部的局部变量
- 不同函数内部声明的变量无法互相访问
- 函数执行完毕后,函数内部的变量实际被清空了
块作用域
在 JavaScript 中使用 { } 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问
- let 声明的变量会产生块作用域,var 不会产生块作用域
- const 声明的常量也会产生块作用域
- 不同代码块之间的变量无法互相访问
- 综上所述,推荐使用const 或 let
全局作用域
<script> 标签 和 .js 文件 的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。全局作用域中声明的变量,任何其它作用域都可以被访问
注:
- 为 window 对象动态添加的属性默认也是全局的,不推荐!
- 函数中未使用任何关键字声明的变量为全局变量,不推荐!
- 尽可能少的声明全局变量,防止全局变量被污染
1.2、作用域链
作用域链本质上是底层的变量查找机制
- 在函数被执行时,会优先查找当前函数作用域中查找变量
- 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
小结:
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
1.3、JS垃圾回收机制
什么是垃圾回收机制?
垃圾回收机制(Garbage Collection) 简称 GC
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收
内存的生命周期
JS环境中分配的内存, 一般有如下生命周期:
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
注:
- 全局变量一般不会回收(关闭页面回收);
- 一般情况下局部变量的值,不用了,会被自动回收掉
内存泄漏:程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄露
1.4、闭包
概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
闭包 = 内层函数 + 引用外层函数的变量
<script>
function outer() {
let num = 10
function inner() {
// 闭包 内层函数使用外层函数变量
console.log(num)
}
inner()
}
outer()
</script>
闭包作用:封闭数据(实现数据私有),提供操作,外部也可以访问/使用函数内部的变量
闭包的基本格式:
<script>
function outer() {
let a = 100
function fn() {
console.log(a)
}
return fn
}
// outer() === fn() === function fn() {}
const fun = outer()
fun() // 100
</script>
闭包可能会引起的问题?造成内存泄漏
1.5、变量与函数提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)
变量提升流程
- 先把var 变量提升到当前作用域于最前面
- 只提升变量声明, 不提升变量赋值
注:
- 变量在var声明之前即被访问,变量的值为
undefined
- 变量提升出现在相同作用域当中
- let/const 声明的变量不存在变量提升
- 实际开发中推荐先声明再访问变量
2、函数与解构赋值
2.1、函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用
<script>
// 会把所有函数声明提升到当前作用域的最前面
// 只提升函数声明 不提升函数调用
fn()
function fn() {
console.log('函数提升')
}
foo() // // 函数表达式必须先声明和赋值 后调用 否则报错 foo is not function
var foo = function () {
console.log('函数表达式');
}
foo()
</script>
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
2.2、函数参数
动态参数
arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参
<script>
function getSum() {
let sum = 0
// console.log(arguments)
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i]
}
console.log(sum);
}
getSum(5, 6) // 11
getSum(8, 8, 4, 8) // 28
</script>
- arguments 是一个伪数组,只存在于函数中
- arguments 的作用是动态获取函数的实参
- 可以通过for循环依次得到传递过来的实参
剩余参数
剩余参数允许我们将一个不定数量的参数表示为一个数组
<script>
function getSum(x, y, ...other) {
let sum = 0
// console.log(other)
for (let i = 0; i < other.length; i++) {
sum += other[i]
}
console.log(x, y)
console.log(sum)
}
getSum(6, 6) // 0
getSum(5, 9, 4, 3) // 7
</script>
arguments 与 剩余参数区别
- … 是语法符号,置于函数形参之后,用于获取多余的实参
- 借助 … 获取的剩余实参,是个真数组
- 动态参数是伪数组(把传递的值全部获取了)
展开运算符
展开运算符(…),将一个数组进行展开
<script>
let arr = [56, 84, 265, 96, 66, 49]
console.log(...arr)
</script>
注:不会修改原数组
应用场景:求数组最大值/最小值、合并数组
<script>
let arr = [56, 84, 265, 96, 66, 49]
console.log(...arr)
// 求最大值最小值
console.log(Math.max(...arr)) // 265
console.log(Math.min(...arr)) // 49
let arr1 = [1, 2, 3]
let arr2 = [99, 98, 97]
// 合并数组
let newArr = [...arr1, ...arr2]
console.log(newArr)
</script>
展开运算符 or 剩余参数
- 剩余参数:函数参数使用,得到真数组
- 展开运算符:数组中使用,数组展开
2.3、箭头函数
引入箭头函数的目的是更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁
使用场景:箭头函数更适用于那些本来需要匿名函数的地方
基本使用
基本写法
<script>
const fn = () => {
console.log(123)
}
fn()
</script>
只有一个参数可以省略小括号
<script>
const fn = x => {
console.log(x)
}
fn(52)
</script>
如果函数体只有一行代码,可以写到一行上,并且无需写 return 直接返回值
<script>
const fn = (x, y) => x + y
const result = fn(88, 12)
console.log(result)
</script>
加括号的函数体返回对象字面量表达式
<script>
const fn = (pwd) => ({ password: pwd })
console.log(fn('8848'))
</script>
箭头函数参数
- 普通函数有arguments 动态参数
- 箭头函数没有 arguments 动态参数,但是有 剩余参数
...args
<script>
const getSum = (...args) => {
let sum = 0
for (let i = 0; i < args.length; i++) {
sum += args[i]
}
return sum
}
const result = getSum(99, 1, 98, 2, 97, 3)
console.log(result)
</script>
箭头函数 this
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值,非常令人讨厌。箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this
<script>
const obj = {
uname: 'Jack',
sayHi: () => console.log(this) // 此时this指向window
}
obj.sayHi()
</script>
2.4、结构赋值
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法
<script>
const arr = [80, 60, 70]
// 数组解构
const [max, min, avg] = arr
console.log(max) // 80
console.log(min) // 60
console.log(avg) // 70
// 典型应用 交换两个变量
let addr1 = '郑州'
let addr2 = '北京';
[addr2, addr1] = [addr1, addr2]
console.log(addr1, addr2)
</script>
- 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
- 变量的顺序对应数组单元值的位置依次进行赋值操作
js 前面必须加分号情况
- 立即执行函数
- 数组解构
变量多 单元值少的情况:
<script>
const [a, b, c, d] = [11, 22, 33]
console.log(a, b, c, d) // 11 11 22 d 为 undefined
</script>
变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
变量少 单元值多的情况:
<script>
const [x, y] = [11, 22, 33]
console.log(x, y) // 11 22
</script>
利用剩余参数解决变量少 单元值多的情况:
<script>
const [a, b, ...args] = [11, 22, 33, 44, 55, 66]
console.log(a, b) // 11 22
console.log(args) // [33 44 55 66]
</script>
注:剩余参数依然返回的还是一个真数组!
防止有undefined传递单元值的情况,可以设置默认值:
<script>
const [a = 0, b = 0] = [11]
console.log(a, b) // 11 0
</script>
按需导入,忽略某些返回值:
<script>
const [x, , z] = [11, 22, 33]
console.log(x, z) // 11 33
</script>
支持多维数组的结构:
<script>
const [a, b, [c, d]] = [11, 22, [33, 44]]
console.log(a, b) // 11 22
console.log(c, d) // 33 44
</script>
对象解构
对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法
<script>
const { uname, age } = { uname: 'Amy', age: 20 }
// 要求属性名和变量名必须一致才行 否则 undefined
console.log(uname, age)
</script>
- 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
- 对象属性的值将被赋值给与属性名相同的变量
- 对象中找不到与变量名一致的属性时变量值为 undefined
给新的变量名赋值:
<script>
// 把原来的 name 变量重新命名为 username
const { uname: username, age } = { uname: 'Amy', age: 20 }
console.log(username, age)
</script>
冒号表示“什么值:赋值给谁”
数组对象解构
<script>
const goods = [
{
goodsName: '红米K40',
price: 2599,
color: '静谧灰'
},
]
const [{ goodsName, price, color }] = goods
console.log(goodsName, price, color)
</script>
多级对象解构
<script>
const info = {
"code": 200,
"msg": "获取图书信息成功",
"data": [
{
"id": 1001,
"url": 'https://img11.360buyimg.com/n1/s200x200_jfs/t1/201380/8/19336/82309/61c41c65E7ddfc9b9/b3e47e2171d4a6d7.jpg',
"title": 'Vue.js 3前端开发实战',
"price": 39.9
},
{
"id": 1002,
"url": 'https://img12.360buyimg.com/n1/s200x200_jfs/t1/25586/6/12013/353666/5c946c61Ed5674914/114c8b9389a9cca8.jpg.avif',
"title": 'Spring Boot编程思想(核心篇)(博文视点出品)',
"price": 59.9
},
{
"id": 1003,
"url": 'https://img10.360buyimg.com/n1/s200x200_jfs/t1/33599/10/15885/167479/5d2553a6Ede24df84/cb3259b561bf74f8.jpg.avif',
"title": '微服务 架构师',
"price": 138.5
},
{
"id": 1004,
"url": 'https://img11.360buyimg.com/n1/s200x200_jfs/t1/84136/38/22081/39432/62fe0357Ed21f6f82/ae099e98a92ab2c0.jpg.avif',
"title": 'MySQL是怎样运行的 从根儿上理解',
"price": 49.9
},
]
}
const { data } = info
console.log(data)
</script>
遍历数组 forEach 方法
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数
应用场景:遍历数组的每个元素
具体语法:被遍历的数组.forEach(function (当前数组元素,当前元素索引号) { // 函数体 })
<script>
data = [
{
"id": 1,
"uname": '小白',
},
{
"id": 2,
"uname": '晓琳',
},
{
"id": 3,
"uname": '小昭',
}
]
// 遍历数据
data.forEach(function (ele, index) {
console.log(ele, index)
})
</script>
注:参数当前数组元素是必须要写的, 索引号可选
筛选数组 filter 方法
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
应用场景:筛选数组符合条件的元素,并返回筛选之后元素的新数组
返回值:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组
因为返回新数组,所以不会影响原数组
具体语法:被遍历的数组.filter(function (currentValue,index) { // return 筛选条件 })
<script>
const arr = [266, 399, 566, 1999, 699]
// 筛选出数组中大于566的元素
const newArr = arr.filter(function (ele,index) {
return ele > 566
})
console.log(newArr) // [1999, 699]
</script>
注:currentValue 必须写, index 可选
2、面向对象
2.1、创建对象三种方式
通过对象字面量创建对象
<script>
const user = {
uname: '小白',
age: 22,
email: 'xiaobai@qq.com'
}
console.log(user)
</script>
通过 new Object 创建对象
<script>
const obj = new Object({ uname: '小昭', age: 20 })
obj.address = '河南郑州'
console.log(obj)
</script>
通过构造函数创建对象
<script>
function Person(uname, age, email) {
this.uname = uname
this.age = age
this.email = email
}
const xiaobai = new Person('小白', 23, 'xiaobai@qq.com')
console.log(xiaobai)
const xiaozhao = new Person('小昭', 22, 'xiaozhao@qq.com')
console.log(xiaozhao)
const xiaolin = new Person('晓琳', 20, 'xiaolin@qq.com')
console.log(xiaolin)
</script>
2.2、构造函数
构造函数 :是一种特殊的函数,主要用来初始化对象
使用场景:常规的 {…} 语法允许创建一个对象。比如我们创建了小白的对象,继续创建小昭的对象还需要重新写一遍,此时可以通过构造函数来快速创建多个类似的对象
<script>
// 创建商品构造函数
function Goods(name, price, count) {
this.name = name
this.price = price
this.count = count
}
const mi = new Goods('红米K40', 2599, 1000)
const hw = new Goods('华为/HUAWEI P6', 4488, 500)
// ...
</script>
构造函数在技术上是常规函数。 不过有两个约定:
- 它们的命名以大写字母开头
- 它们只能由 “new” 操作符来执行
- 使用 new 关键字调用函数的行为被称为实例化
- 实例化构造函数时没有参数时可以省略 ()
- 构造函数内部无需写return,返回值即为新创建的对象
- 构造函数内部的 return 返回的值无效,所以不要写return
实例化执行过程
- 创建新空对象
- 构造函数this指向新对象
- 执行构造函数中的代码,添加新的属性和方法
- 返回新对象
构造函数数据封装
封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现数据封装
<script>
// 公共的属性和方法
function Person(name, age, address) {
this.name = name
this.age = age
this.address = address
this.sleep = function () {
console.log('I can sleep')
}
}
const xiaozhao = new Person('小赵', 25, '河南洛阳')
const xiaohei = new Person('小黑', 22, '北京海淀')
console.log(xiaozhao === xiaohei) // false
xiaozhao.sleep()
console.log(xiaozhao.sleep === xiaohei.sleep) // false 说明两个函数不一样
</script>
- 构造函数体现了面向对象的封装特性
- 构造函数实例创建的对象彼此独立、互不影响
构造函数方法虽好用,但是 存在内存浪费的问题
希望所有的对象使用同一个函数,这样就比较节省内存,如何实现呢?原型
2.3、实例成员静态成员
实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员
<script>
function Person(uname, age, email) {
this.uname = uname
this.age = age
this.email = email
}
const xiaobai = new Person('小白', 23, 'xiaobai@qq.com')
console.log(xiaobai)
const xiaozhao = new Person('小昭', 22, 'xiaozhao@qq.com')
console.log(xiaobai === xiaozhao) // false
// 实例属性
xiaobai.age = 30
// 实例方法
xiaobai.speak = () => console.log('I can speak English')
console.log(xiaobai)
console.log(xiaozhao)
</script>
- 为构造函数传入参数,动态创建结构相同但值不同的对象
- 实例对象彼此独立互不影响
静态成员
构造函数的属性和方法被称为静态成员
<script>
function Person(uname) {
this.uname = uname
}
// 静态属性
Person.address = '河南郑州'
console.log(Person.address)
// 静态方法
Person.sing = function () {
console.log('I can sing')
}
Person.sing()
</script>
- 静态成员只能通过构造函数来访问
- 静态方法中的this指向构造函数
2.4、编程思想
面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了
面向过程,就是按照分析好了的步骤,按照步骤解决问题
面向对象编程(OOP)
面向对象是把事务分解成为一个个对象,然后由对象之间分工合作
面向对象是以对象功能来划分问题,而不是步骤
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工
面向对象三大特性:
- 封装性
- 继承性
- 多态性
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目
面向过程 VS 面向对象
面向过程编程
优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程
缺点:没有面向对象易维护、易复用、易扩展
面向对象编程
优点:易维护、易复用、易扩展,由于面向对象有封装 、继承、多态性的特性,可以设计出低耦合的系统,使 系统 更加灵活、更加易于维护
缺点:性能比面向过程低
没有最好的语言,只有适用于不同场景的语言
2.5、原型
构造函数通过原型分配的函数是所有对象所共享的
JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
这个对象可以挂载函数,对象实例化不会多次创建原型上函数,从而节约内存
我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法
构造函数和原型对象中的this 都指向 实例化的对象
<script>
// 公共的属性写在构造函数里面
function Person(name, age, address) {
this.name = name
this.age = age
this.address = address
}
// 公共的方法写在原型对象身上
Person.prototype.sleep = function () {
console.log('I can sleep')
}
const xiaozhao = new Person('小赵', 25, '河南洛阳')
const xiaohei = new Person('小黑', 22, '北京海淀')
console.log(xiaozhao.sleep === xiaohei.sleep) // true 说明两个函数一样 共享
</script>
构造函数和原型对象中的this 指向
构造函数
<script>
let that
function Person(name) {
that = this
this.name = name
}
// 构造函数里面的 this 就是 实例对象 xiaobai
const xiaobai = new Person('小白')
console.log(that === xiaobai) // true
</script>
原型对象
<script>
let that
function Person(name) {
this.name = name
}
// 原型对象里面的函数 this 指向的还是 实例对象 xiaobai
Person.prototype.sleep = function () {
that = this
console.log('I can sleep')
}
const xiaobai = new Person('小白')
xiaobai.sleep()
console.log(that === xiaobai) // true
</script>
构造函数和原型对象中的 this 都指向 实例化的对象
扩展日期方法
<script>
Date.prototype.getCurrentStringDate = function () {
// 原型对象里面的this指向? 当前的实例对象 date
// console.log(this)
const year = this.getFullYear()
const month = this.getMonth() + 1
const date = this.getDate()
let hour = this.getHours()
let minutes = this.getMinutes()
let seconds = this.getSeconds()
// 补零操作
hour = fillZero(hour)
minutes = fillZero(minutes)
seconds = fillZero(seconds)
return `${year}年${month}月${date}日 ${hour}时${minutes}分${seconds}秒`
}
function fillZero(value) {
return value < 10 ? '0' + value : value
}
const date = new Date()
console.log(date.getCurrentStringDate()) // 2022年3月16日 13时10分04秒
</script>
constructor 属性
每个原型对象(prototype)/对象原型(_proto_)里面都有个constructor 属性(constructor 构造函数)
作用:该属性指向该原型对象的构造函数
应用场景:如果有多个对象的方法,可以给原型对象采取对象形式赋值,但这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了,此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数
<script>
function Person(name) {
this.name = name
}
Person.prototype = {
// 重新指回创造这个原型对象的 构造函数
constructor: Person,
eat: function () {
console.log('I can eat')
},
sleep: function () {
console.log('I can sleep')
},
}
console.log(Person.prototype.constructor) // 指向 Person
console.log(Person.prototype.constructor === Person) // true
</script>
对象原型
对象都会有一个属性 proto 指向构造函数的 prototype 原型对象**(对象原型指向原型对象)**,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在
注:
- _proto_ 是JS非标准属性
- [[prototype]]和__proto__意义相同
- 用来表明当前实例对象指向哪个原型对象prototype
- __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性
封装-抽取公共部分
把猫和狗的公共的部分抽取出来放到动物里面
<script>
const Animal = {
eyes: 2,
mouth: 1
}
// 猫 构造函数
function Cat() {
}
// Cat 通过原型继承 Animal
Cat.prototype = Animal
// 指回原来的构造函数
Cat.prototype.constructor = Cat
// 给猫咪添加一个上树的方法
Cat.prototype.upTree = function () {
console.log('I can upTree')
}
const mengmeng = new Cat()
console.log(mengmeng)
console.log(Cat.prototype)
// 狗 构造函数
function Dog() {
}
Dog.prototype = Animal
Dog.prototype.constructor = Dog
const xiaohei = new Dog()
console.log(xiaohei)
</script>
产生的问题:如果给猫添加了一个上树的方法,发现狗自动也添加了这个方法
原因:由于猫和狗都同时使用了同一个对象,根据引用类型的特点,他们指向同一个对象,修改一个就会都影响
解决:猫和狗不能使用同一个对象,但是不同对象里面包含相同的属性和方法
构造函数 new 每次都会创建一个新的对象
<script>
// 构造函数 new出来的对象 每一次都不一样
function Animal(eyes, mouth) {
this.eyes = eyes
this.mouth = mouth
}
// 猫 构造函数
function Cat() {
}
// Cat 通过原型继承 Animal 构造函数的属性
Cat.prototype = new Animal()
// 指回原来的构造函数
Cat.prototype.constructor = Cat
// 给猫咪添加一个上树的方法
Cat.prototype.upTree = function () {
console.log('I can upTree')
}
const mengmeng = new Cat()
console.log(mengmeng)
// 狗 构造函数
function Dog() {
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
const xiaohei = new Dog()
console.log(xiaohei)
</script>
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链
- 所有对象的里面都有_proto_,对象原型指向原型对象
- 只要是原型对象里面就有constructor,指向创建该原型对象的构造函数
原型链-查找规则
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性
- 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
- 如果还没有就查找原型对象的原型(Object的原型对象)
- 依此类推一直找到 Object 为止(null)
可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
<script>
function Person() { }
const xb = new Person()
console.log(xb instanceof Person) // true
console.log(xb instanceof Object) // true
console.log(xb instanceof Array) // false
console.log(Array instanceof Object) // true
</script>
3、内置构造函数
在 JavaScript 中最主要的数据类型有 6 种:
基本数据类型:
- 字符串、数值、布尔、null、undefined
引用类型:
- 数组、对象、函数
JS中几乎所有的数据都可以基于构造函数创建
引用类型:Object,Array,RegExp,Date 等
包装类型:String、Number、Boolean 等
3.1、Number
Number 是内置的构造函数,用于创建数值
常用方法: toFixed() 设置保留小数位的长度
<script>
const price = 66.655
// 保留两位小数 且四舍五入
console.log(price.toFixed(2)) // 66.66
</script>
3.2、String
在 JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法
<script>
const str = 'hello'
console.log(str.length)
// js底层完成的,把简单数据类型包装成了引用数据类型
// const str = new String('hello')
</script>
之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被 称为包装类型
常用实例方法
- 实例属性 length 用来获取字符串的长度
- split ( ‘分隔符’)用来将字符串拆分成数组
- includes(搜索的字符串[,检测位置索引号])判断一个字符串是否包含在另一个字符串中,根据情况返回true或false
- substring (需要截取的第一个字符的索引[ ,结束的索引号]),用于字符串截取
- startswith(检测字符串[,检测位置索引号]),检测是否以某字符开头
- endsWith检测是否以某字符结尾
- toUpperCase 用于将字母转换成大写
- toLowerCase 用于将字母转换成小写
- indexOf 查找子字符串第一次出现的索引位置,找不到返回 -1
- replace用于替换字符串,支持正则匹配
- match用于查找字符串,支持正则匹配
split
<script>
const str = '2023-05-06'
// 将字符串转换为数组
const arr = str.split('-') // ['2023', '05', '06']
console.log(arr)
</script>
includes
<script>
const str = 'https://www.baidu.com'
// 如果在字符串中包含该 字符 返回true 反之false
const isFind = str.includes('cn') // false
console.log(isFind)
</script>
substring
<script>
const str = 'hello world'
// 截取字符串
console.log(str.substring(6)) // world
console.log(str.substring(0, 5)) // hello
</script>
startswith
<script>
const str = 'https://www.baidu.com'
// 判断字符串是否以给定的字符串开头 根据条件 返回true或false
console.log(str.startsWith('http')) // true
console.log(str.startsWith('www')) // false
</script>
3.3、Array
Array 是内置的构造函数,用于创建数组
<script>
// 通过构造函数创建数组
const arr = new Array([9, 8, 5, 2, 1, 1])
console.log(arr)
</script>
创建数组建议使用字面量创建,不用 Array构造函数创建
常用实例方法
方法 | 作用 | 描述 |
---|---|---|
forEach | 遍历数组 | 无返回值,用于不改变值,经常用于查找打印输出值 |
filter | 过滤数组 | 筛选数组元素,并生成新数组 |
map | 迭代数组 | 返回新数组,新数组里面的元素是处理之后的值,经常用于处理数据 |
reduce | 累计器 | 返回函数累计处理的结果,经常用于求和等 |
作用:reduce 返回函数累计处理的结果,经常用于求和等
具体语法:数组.reduce(function (累计值, 当前元素){},起始值)
<script>
const arr = [9, 8, 5, 2, 1, 1]
// 无初始值
const result = arr.reduce(function (prev, current) {
return prev + current
})
console.log(result)
// 上一次值 当前值 返回值 (第一次循环)
// 9 8 17
// 上一次值 当前值 返回值 (第二次循环)
// 17 5 22
// 上一次值 当前值 返回值 (第三次循环)
// 22 2 24
// 上一次值 当前值 返回值 (第四次循环)
// 24 1 25
// 上一次值 当前值 返回值 (第五次循环)
// 25 1 26
// 有初始值
// const total = arr.reduce(function (prev, current) {
// return prev + current
// }, 20)
// console.log(total)
// 箭头函数
const total = arr.reduce((prev, current) => prev + current, 20)
console.log(total)
</script>
累计值参数:
- 如果没有起始值,则上一次值以数组的第一个数组元素的值
- 每一次循环,把返回值作为下一次循环的上一次值
- 如果有起始值,则起始值作为上一次值
伪数组转换为真数组
静态方法 Array.from()
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
const lis = document.querySelectorAll('ul>li')
console.log(lis)
// 把伪数组转换为真数组
const newLis = Array.from(lis)
newLis.pop()
console.log(newLis)
</script>
其他方法
- 实例方法 join 数组元素拼接为字符串,返回字符串
- 实例方法 find 查找元素,返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回undefined
- 实例方法 some 检测数组中的某个元素是否满足指定条件,如果数组中有元素满足条件返回true,否则返回false
- 实例方法 every 检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回true,否则返回false
- 实例方法 concat 合并两个数组,返回生成新数组
- 实例方法 sort 对原数组单元值排序
- 实例方法 splice 删除或替换原数组单元
- 实例方法 reverse 反转数组
- 实例方法 findIndex 查找元素的索引值
find
<script>
const arr = [
{
name: '华为',
price: 4999
},
{
name: 'OPPO',
price: 1999
},
]
const good = arr.find(ele => ele.name === 'OPPO')
console.log(good)
</script>
every
<script>
const arr = [50, 60, 70]
// every 每一个元素都要符合条件 只要有一个不符合 返回false 反之为true
const flag = arr.every(function (ele) {
return ele >= 60
})
console.log(flag) // false
const isFind = arr.every(ele => ele >= 50)
console.log(isFind) // true
</script>
concat
<script>
let arr1 = ['a', 'b', 'c']
let arr2 = [1, 2, 3]
// 合并两个或多个数组 返回一个新数组
const newArr = arr1.concat(arr2)
console.log(newArr) // ['a', 'b', 'c', 1, 2, 3]
</script>
findIndex
<script>
const arr = ['orange', 'apple', 'banana', 'apple']
// 返回数组中第一个满足条件的索引号 没有找到返回 -1
const index = arr.findIndex(function (ele) {
return ele === 'apple'
})
console.log(index) // 1
const i = arr.findIndex(ele => ele === 'pear')
console.log(i) // -1
</script>
3.4、Object
Object 是内置的构造函数,用于创建普通对象
<script>
// 通过构造函数创建对象
const user = new Object({ uname: '小白', age: 22 })
console.log(user)
</script>
推荐使用字面量方式声明对象,而不是 Object 构造函数
常用静态方法
静态方法就是只有构造函数Object可以调用的
作用:Object.keys 静态方法获取对象中所有属性名(键),Object.values 静态方法获取对象中所有属性值
<script>
const user = new Object({ uname: '小白', age: 22 })
// 获取所有属性名
console.log(Object.keys(user)) // 返回数组 ['uname', 'age']
// 获取所有属性值
console.log(Object.values(user)) // 返回数组 ['小白', 22]
</script>
注: 返回的是一个数组
作用:Object. assign 静态方法常用于对象拷贝
应用场景:给对象添加属性
<script>
const user = new Object({ uname: '小白', age: 22 })
// 对象的拷贝 把 user 拷贝给 obj
const obj = {}
Object.assign(obj, user)
console.log(obj)
// 给 user 动态添加一个属性
Object.assign(user, { email: 'xiaobai@qq.com' })
console.log(user)
</script>
4、深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
常见方法:
- 拷贝对象:Object.assgin() / 展开运算符 {…obj}
- 拷贝数组:Array.prototype.concat() 或者 […arr]
<script>
const obj = {
uname: '小白',
age: 25,
address: {
province: '湖北省',
city: '武汉市',
}
}
console.log(obj)
// 浅拷贝
// const o = { ...obj }
// o.age = 30
// console.log(o)
// console.log(obj)
const o = Object.assign({}, obj)
o.age = 20
obj.address.province = '河南省'
obj.address.city = '洛阳市'
console.log(o)
console.log(obj)
</script>
如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)
深拷贝
深拷贝:拷贝的是对象,不是地址
常见方法:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.stringify()实现
函数递归
函数递归: 如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
<script>
// 使用递归循环10次 打印
let i = 1
function fn() {
console.log(i)
if (i >= 10) return
i++
fn() // 函数内部调用函数自己
}
fn()
</script>
<script>
const obj = {
uname: '小白',
age: 25,
hobbies: ['code', 'gril', 'swim'],
address: {
province: '湖北省',
city: '武汉市'
}
}
const o = {}
// 拷贝函数
function deepCopy(newObj, oldObj) {
for (let k in oldObj) {
// 处理数组的问题
if (oldObj[k] instanceof Array) {
// newObj[k] hobbies
// oldObj[k] ['code', 'gril', 'swim']
newObj[k] = []
deepCopy(newObj[k], oldObj[k])
} else if (oldObj[k] instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], oldObj[k])
} else {
// k 属性名 oldObj[k] 属性值
// newObj[k] === o.uname
newObj[k] = oldObj[k]
}
}
}
deepCopy(o, obj) // 函数调用 o 新对象 obj 旧对象
o.age = 21
o.address.province = '河南省'
o.address.city = '洛阳市'
o.hobbies[0] = 'basketball'
console.log(o)
console.log(obj)
</script>
- 深拷贝需要函数递归
- 如果拷贝基本类型的时候不会出现任何问题,但当遇见复杂数据类型的时候,就需要再次调用这个递归函数,内部进行数据类型判断
- 一定要先处理数组,在处理对象!
lodash
js库lodash里面cloneDeep内部实现了深拷贝
具体语法:_.cloneDeep(要克隆的对象)
<script src="./lib/lodash.min.js"></script>
<script>
const obj = {
uname: '小白',
age: 25,
hobbies: ['code', 'gril', 'swim'],
address: {
province: '湖北省',
city: '武汉市'
}
}
const o = _.cloneDeep(obj)
console.log(o)
o.hobbies[0] = 'basketball'
console.log(obj)
</script>
JSON.stringify
通过JSON.stringify()实现
<script>
const obj = {
uname: '小白',
age: 25,
hobbies: ['code', 'gril', 'swim'],
address: {
province: '湖北省',
city: '武汉市'
}
}
// 把对象转换为json字符串
// console.log(JSON.stringify(obj))
const o = JSON.parse(JSON.stringify(obj))
console.log(o)
o.hobbies[0] = 'basketball'
console.log(obj)
</script>
5、异常处理与处理this
5.1、异常处理
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
了解程序异常处理的方法,从而提升代码运行的健壮性
try/catch捕获异常
try / catch 用于捕获异常信息(浏览器提供的错误信息) try 试试 catch 抓住/捕获 finally 最后
<div class="content">div</div>
<script>
try {
// 可能出现错误的代码 写到try块里
const div = document.querySelector('.contont')
div.style.color = 'orange'
} catch (error) {
// 拦截异常 提示是浏览器提供的错误信息 但是不中断程序
console.log(error.message)
throw new Error('选择器选择出错')
// 需要手动中断程序
// return
} finally {
console.log('不管程序出错与否 我都会执行')
}
console.log('如果程序出现错误 我将不会执行')
</script>
- 将预估可能发生错误的代码写在 try 代码段中
- 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
- finally 不管是否有错误,都会执行
throw抛异常
<script>
function getSum(x, y) {
if (!x || !y) {
throw new Error('求和函数没有参数传递!')
}
return x + y
}
console.log(getSum())
</script>
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象通常配合 throw 使用,能够设置更详细的错误信息
5.2、改变this
this指向
this 是 JavaScript 最具“魅惑”的点,不同的应用场合 this 的取值可能会有意想不到的结果
普通函数的调用方式决定了 this 的值,即**【谁调用 this 的值指向谁】,严格模式下指向 undefined**
箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !
- 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的
- 箭头函数中的this引用的就是最近作用域中的this
- 向外层作用域中,一层一层查找this,直到有this的定义
改变指向
JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向
call方法
使用 call 方法调用函数,同时指定被调用函数中 this 的值
具体语法:fun.call(thisArg, arg1, arg2, ...)
<script>
const obj = {
uname: '小白'
}
function fn(x, y) {
console.log(this) // window
console.log(x + y)
}
// 1.调用函数
// 2.改变this指向
fn.call(obj, 6, 6)
</script>
- thisArg:在 fun 函数运行时指定的 this 值
- arg1,arg2:传递的其他参数
- 返回值就是函数的返回值,因为它就是调用函数
apply方法
使用 apply 方法调用函数,同时指定被调用函数中 this 的值
具体语法:fun.apply(thisArg, [argsArray])
<script>
const obj = {
uname: '小白'
}
function fn(x, y) {
console.log(this)
console.log(x + y)
}
// 1.调用函数
// 2.改变this指向
fn.apply(obj, [6, 6])
// 应用场景:求数组最大值
const arr = [99, 88, 100, 66]
const max = Math.max.apply(Math, arr)
console.log(max)
</script>
- thisArg:在fun函数运行时指定的 this 值
- argsArray:传递的值,必须包含在数组里面
- 返回值就是函数的返回值,因为它就是调用函数
- 因此 apply 主要跟数组有关系,比如使用 Math.max() 求数组的最大值
bind方法
bind() 方法不会调用函数。但是能改变函数内部this 指向
具体语法:fun.bind(thisArg, arg1, arg2, ...)
<script>
const obj = {
uname: '小白'
}
function fn() {
console.log(this)
}
// 1.不会调用函数
// 2.可以改变this指向
// 3.返回值是个函数 但是该函数里面的this是更改过的 obj
const foo = fn.bind(obj)
foo()
</script>
- thisArg:在 fun 函数运行时指定的 this 值
- arg1,arg2:传递的其他参数
- 返回由指定的 this 值和初始化参数改造的 原函数拷贝 (新函数)
- 因此当我们只是想改变 this 指向,并且不想调用这个函数的时候,可以使用 bind,比如改变定时器内部的 this指向
call apply bind三者区别
相同点:都可以改变函数内部的this指向
不同点:
- call 和 apply 会调用函数,并且改变函数内部this指向
- call 和 apply 传递的参数不一样,call 传递普通参数 arg1, arg2…形式,apply 必须数组形式[arg]
- bind 不会调用函数,可以改变函数内部this指向
应用场景:
- call 调用函数并且可以传递普通参数
- apply 经常跟数组有关系,比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还能改变this指向,比如改变定时器内部的this指向
5.3、节流与防抖
节流
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数,可以减少一段时间内事件的触发频率
应用场景:轮播图点击效果
假如一张轮播图完成切换需要300ms, 不加节流效果,快速点击,则快速的切换效果体验很不好!
加上节流效果, 不管快速点击多少次, 300ms时间内,只能切换一张图片
通过lodash库实现节流
<script src="../lib/lodash.min.js"></script>
<script>
// 语法: _.throttle(fun,等待时长)
pic.addEventListener('click', _.throttle(picToggle, 1000))
</script>
手写节流函数
核心思路:核心是通过setTimeout定时器来完成
- 声明一个定时器变量
- 当鼠标每次点击都先判断是否有定时器了,如果有定时器则不开启定时器
- 如果没有定时器,则开启定时器,把定时器存放到变量中
- 在定时器里面调用要执行的函数
- 定时器里面要把定时器清空
<script>
function throttle(fn, t) {
let timerId = null
return function () {
if (!timerId) {
timerId = setTimeout(function () {
fn()
// 清空定时器
timerId = null
}, t)
}
}
}
</script>
防抖
防抖(debounce)是当事件被触发后,延迟 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时
应用场景:搜索框防抖
假设输入就可以发送请求,但是不能每次输入都去发送请求,输入比较快发送请求会比较多,造成服务器压力过大!
假如300ms, 当输入第一个字符时候,300ms后发送请求,但是在200ms的时候又输入了一个字符, 则需要再等300ms 后发送请求,可以通过防抖策略,只在输入完后,才执行查询的请求,这样可以有效减少请求次数,节约请求资源
通过lodash库实现防抖
<script src="../lib/lodash.min.js"></script>
<script>
// 语法:_.debounce(fun, 等待时长)
ipt.addEventListener('mousemove', _.debounce(input, 300))
</script>
手写防抖函数
核心思路:核心是通过setTimeout定时器来完成
- 声明一个定时器变量
- 当鼠标每次滑动都先判断是否有定时器了,如果有定时器先清除定时器
- 如果没有定时器,则开启定时器,把定时器存放到变量中
- 在定时器里面调用要执行的函数
<script>
function debounce(fn, t) {
let timerId
// return 返回一个匿名函数
return function () {
if (timerId) clearTimeout(timerId)
timerId = setTimeout(function () {
fn() // 加小括号调用 fn 函数
}, t)
}
}
</script>