前端 --- JS 基础语法

0. 官方网站

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript

1.简介

1.1 组成部分

  1. ECMAScript -- JavaScript的语言基础
  2. Web APIs -- 接口( DOM(页面文档对象模型) 和 BOM(浏览器对象模型))

2. 语法规范

2.1 引入方式

1 内部JS

直接写在html文件里,用script标签包裹,在</body>的上面,为了代码的执行顺序,要先有标签和样式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div class="outer"></div>

 <!-- 内部JS,写在</body>的上面 -->
<script>
    let a = 10
    console.log(a)
</script>
</body>
</html>

2. 外部JS

代码写在以.js结尾的文件中,然后使用script标签,引入到html页面中

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div class="outer">
</div>
    
<!-- 外部JS,写在</body>的上面  -->
<script src="./js/index.js"></script>
</body>
</html>

3. 内联JS

代码写在标签内部

<button onclick="alert('内联JS')">弹窗</button>

2.2 注释和结束符

1. 单行注释

// 单行注释

2. 多行注释

/* 多
   行
   注
   释 */

3. 结束符

alert(1);  // 以分号作为结束符,可以不加,js会自动加
alert(2);
alert(3);

2.3 输入和输出

1. 输入

// 1. 显示一个对话框,提供给用户输入内容
prompt("请输入您的姓名")

2. 输出

// 1. 向body中输出内容
document.write("我是div标签")
document.write("<h1>我是div标签</h1>") // 可以写标签

// 2. 页面弹出警告对话框
alert("123")

// 3. 输出到控制台,程序员调试使用
console.log("123")

3. 变量

用来临时储存数据的容器

3.1 命名规范

  1. 不能使用javascript 内置的关键字,如: let,var,if,function,for等
  2. 只能用下划线、数字、字母、$组成,不能以数字开头
  3. 字母严格区分大小写
  4. 起名要有意义
  5. 遵守小驼峰命名法:第一个单词首字母小写,后面每个单词大写,如userName

3.2 声明、赋值、使用、重新赋值

let a  // 声明变量
a = 1  // 给变量a 赋值

// 声明并赋值(初始化)
let a = 1

// 一次性声明多个变量
let a = 1, b = 2

// 用变量名来使用变量
alert(a)

// 更新赋值变量a
a = 2
alert(a)

3.3 变量交换

核心思路: 使用一个临时变量作中间存储

let num1 = 10
let num2 = 20
let tmp
tmp = num1
num1 = num2
num2 = tmp
console.log(num1, num2)

3.4 let和var 的区别

在比较旧的JavaScript,使用关键字 var 来声明变量,而不是let

现在开发中,一般不使用var,let是var的新一代声明方式,解决了 var 存在的一些问题

使用var声明存在的一些问题:

  1. 可以先使用,后声明,有变量提升的问题
  2. var声明过的变量可以重复声明
  3. 全局变量
  4. 没有块级作用域等

以后得变量声明统一使用let

4. 常量

当某个变量,永远不会发生改变时,可以将其声明成常量

4.1 声明

// 必须声明的时候就赋值,通常使用全部大写的常量名
const USERNAME = "小茗同学"
console.log(userName)

// 不允许修改,这里修改常量的时候会报错
USERNAME = "小茗同学2"

5. 运算符

5.1 算数运算符

  1. 求和: +
  2. 求差: -
  3. 求积: *
  4. 求商: /
  5. 取模(取余数): %

5.2 赋值运算符

let num = 1 // 给变量赋值

num += 1 // num自加1,并重新赋值给num
num -= 1 // num自减1,并重新赋值给num
num *= 1 // num自乘1,并重新赋值给num
num /= 1 // num自除1,并重新赋值给num

5.3 一元运算符

在 JavaScript 的运算符中,可以根据所需表达式的个数,分为一元运算符、二元运算符、三元运算符

1. 一元运算符

正号/负号

let num = 1

++num  // 前置自增,每次运算只能加1,相当于num += 1
--num  // 前置自减,每次运算只能减1,相当于num += 1

num++  // 后置自增,每次运算只能加1,相当于num += 1
num--  // 后置自减,每次运算只能加1,相当于num += 1


前置自增和后置自增的区别?

  1. 单独使用的时候是没有区别的
  2. 如果参与了运算就有区别了
    1. ++num + 2 --- 结果为4, 运行时,++num要先加1,然后++num执行完毕后的值再继续运算,即num先加1,++num的运算结果为2,然后再加2,结果为4,
    2. num++ + 2 --- 结果为3, 运行时,num++,要先参与之后的运算,结束后再执行自加运算,num++的值为1,此时num需要自加1,num的值为2,,由于num++的值为1,所以运算的时候是1+2后的结果为3,此时整个运算的结果为3,num的值为2,

2. 二元运算符

let num = 10 + 20  		// 两个数参与运算

5.4 比较运算符

>      // 运算符左边是否大于右边
<      // 运算符左边是否小于右边
>=     // 运算符左边是否大于或等于右边
<=     // 运算符左边是否小于或等于右边
==     // 运算符左右两边的值是否相等,有隐式转换,会将字符串'2'自动转换成数字2
===    // 运算符左右两边是否类型和值均相等
!=     // 运算符左右两边是否值不相等
!==    // 运算符左右两边是否类型和值均不相等

5.5 逻辑运算符

&&     // 与
     
||     // 或

!      // 非

5.6 运算符的优先级

优先级 运算符 运算顺序
1 小括号 ()
2 一元运算符 ++ -- !
3 算数运算符 先 * / % 后 + -
4 关系运算符 > >= < <=
5 相等运算符 == != === !==
6 逻辑运算符 先 && 后 ||
7 赋值运算符 =
8 逗号运算符 ,

6. 流程控制

1.if

1. 单分支

if () {
    
}

2. 双分支

if () {
    
}else{
    
}

3. 多分支

let num = +prompt("请输入成绩")
if (num >= 90) {
    alert("优秀")
} else if (num >= 70 && num < 90) {
    alert("良好")
} else if (num >= 60 && num < 70) {
    alert("几个")
} else if (num < 60) {
    alert("不及格")
} else {
    alert("输入错误")
}

2.switch

let num = +prompt("请输入数字")
switch (num) {
    case 1:
        alert("1")
        break   // 需要手动退出switch语句
    case 2:
        alert("2")
        break
    case 3:
        alert("3")
        break
    default:
        alert("输入错误")
        // break   // 这里的break可以省略
}

3. if 多分支和 switch的区别

1. 共同点

  1. 都能实现多分支选择,较为常用的是if语句
  2. 大部分情况下都能实现相同逻辑

2. 区别

  1. switch...case语句通常处理case为比较确定值的情况,而if...else...语句更加灵活,通常用于范围判断(大于、等于某个范围)
  2. Switch语句进行一次case判断后直接执行到程序的语句,效率更高,而if...else...语句有几种判断条件就得判断多少次
  3. switch一定要注意必须是 === 全等,一定注意数据类型,同时注意break,否则会有穿透效果

3. 结论

  1. 当分支较少时,if...else...执行效率高
  2. 当分支较多时,switch语句执行效率高,而且结构更清晰

4. 三元运算符

let num = 3
num > 5 ? alert("大于5") : alert("小于5")

1. 数字补0案例

num < 10 ? alert( "0" + String(num)) : alert(num)

5. 断点调试

  1. F12 打开开发者工具
  2. 点到sources一栏
  3. 选择想要调试的那一行代码,在前面的行数点击,然后刷新页面

6. while 循环

let num = 1
while (num < 6) {
    console.log(5)
    num++
}

continue

let num = 0
while (num < 101) {
    if (num === 2) {
        num++       // 需要num++,否则num一直是2,会死循环
        continue   // 结束本次循环
    }
    num++
}

break

let num = 1
while (num < 101) {
    console.log(num)
    if (num === 2) {
        break   // 终止整个循环
    }
    num++
}

综合案例

if 判断实现

let num = 0
while (true) {
    let input = +prompt(`请选择操作:
        1. 存钱
        2. 取钱
        3. 查看余额
        4. 退出`)
    if (input === 4){
        break
    }else if (input === 1){
        money = +prompt("请输入存多少钱")
        if (money <= 0){
            alert("输入错误")
        }else{
            num += money
            continue
        }
    }else if( input === 2){
        money = +prompt("请输入取多少钱")
        if (money <= 0){
            alert("输入错误")
        }else{
            num -= money
            continue
        }
    }else if( input === 3){
        alert(num)
        continue
    }else{
        alert("输入错误")
    }

}

switch 实现

let num = 0
let flag = true
while (flag) {
    let input = +prompt(`请选择操作:
        1. 存钱
        2. 取钱
        3. 查看余额
        4. 退出`)

    switch (input){
        case 1:
            money = +prompt("请输入存多少钱")
            if (money <= 0){
                alert("输入错误")
            }else{
                num += money
            }
            break
        case 2:
            money = +prompt("请输入取多少钱")
            if (money <= 0){
                alert("输入错误")
            }else{
                num -= money
                continue
            }
            break
        case 3:
            alert(num)
            break
        case 4:
            flag = false
            break
        default:
            alert("输入错误")
    }

}
let num = 0
while (flag) {
    let input = +prompt(`请选择操作:
        1. 存钱
        2. 取钱
        3. 查看余额
        4. 退出`)
    if (input === 4){
        break
    }
    switch (input){
        case 1:
            money = +prompt("请输入存多少钱")
            if (money <= 0){
                alert("输入错误")
            }else{
                num += money
            }
            break
        case 2:
            money = +prompt("请输入取多少钱")
            if (money <= 0){
                alert("输入错误")
            }else{
                num -= money
                continue
            }
            break
        case 3:
            alert(num)
            break
        default:
            alert("输入错误")
    }
}

7. for 循环

for (let num = 1; num < 6; num++) {
    console.log(num)
}

for...of...

用来循环对象

const arr1 = ["1","2","3"]
for (let g of arr1){
    console.log(g)
}

continue

for (let num = 1; num < 101; num++) {
    if (num === 2) {
        continue   // 结束本次循环
    }
    console.log(num)
}

break

for (let num = 1; num < 101; num++) {
    if (num === 2) {
        break   // 终止整个循环
    }
    console.log(num)
}

循环嵌套

for (let i = 1; i < 10; i++) {
    let row = ""
    for (let j = 1; j <= i; j++) {
        row += `${i} * ${j} = ${i * j}    `
    }
    console.log(row)
}

7. 基本数据类型

7.1 数字型

1. 整数

let num = 11

2. 小数

let num = 1.1

3. 正数

let num = 11

4. 负数

let num = -11

5. 保留小数位的长度

let num = 11.4351324
num.toFixed(2)  // 四舍五入

7.2 字符串

1. 声明

通过单引号(')、双引号(")、反引号(``)引起来的内容就是字符串

let str1 = '1'
let str2 = "1"
let str3 = `1`

2. 拼接

let a = "晓" + "明"

3. 模版字符串

let age = 18
// 外面必须是反引号,${变量名}来使用变量
console.log(`大家好,我今年${age}岁了`)

4. 替换

const str = "java是一门编程语言,可以做JAVA开发工程师"

// 可以直接写需要替换的字符串,只替换找到的第一个
const res = str.replace("java","js")
console.log(newStr)

// 可以写正则匹配规则,i表示不区分大小写,g表示返回所有匹配的结果,会替换所有的java
const res = str.replace(/java/ig,"js")
console.log(newStr)

5. 长度

const str = "java是一门编程语言,可以做JAVA开发工程师"
console.log(str.length)

6. 分割

const str = "java是一门编程语言,可以做JAVA开发工程师"
const result = str.split(",")   // 根据逗号分割,返回一个数组

7. 截取一段

const str = "java是一门编程语言,可以做JAVA开发工程师"
const result = str.substring(0,5)   // 从索引为0开始,截取5个字符

8. 是否以指定字符开头 / 结尾

const str = "java是一门编程语言,可以做JAVA开发工程师"
const result1 = str.startsWith("j")
const result2 = str.endsWith("师")

9. 是否包含指定字符串

 const str = "java是一门编程语言,可以做JAVA开发工程师"
 const result1 = str.includes("java",str)  // 返回的是布尔值
 const result2 = str.match("一门")  // 如果找到则返回一个数组,每找到返回null
 const result3 = str.indexOf("一")  // 查找指定字符串的索引

10. 大小写转换

const str = "java是一门编程语言,可以做JAVA开发工程师"
const result1 = str.toUpperCase()  // 字符串中的字母全部大写
const result2 = str.toLowerCase()  // 字符串中的字母全部小写

7.3 布尔

let is_login = true

let is_manger = false

7.4 未定义类型(undefined)

如果只声明了变量,并未赋值,那么这个变量的默认值为undefined,一般很少为某个变量赋值为 undefined

7.5 空类型(null) 是个 object 对象类型

let obj = null

7.6 undefined和null的区别

  1. undefined 表示没有赋值

  2. null 表示赋值了,但是内容为空

  3. 计算时有区别

    undefined + 1  // NaN  -- Not a Number
    
    null + 1  // 1
    

8. 引用数据类型

数组和对象用 const 来声明

8.1 数组

用来存储多个不同数据类型的集合

1. 声明

// 使用数组字面量声明
const arry1 = ["1",1,[3,"5"]]
console.log(arry1)

// 使用构造函数声明数组
const  arry2 = new Array("1",1,[3,"5"])
console.log(arry2)

2. 索引取值

const  arry1 = ["1",1,[3,"5"]]
console.log(arry1[0])  // 索引从0开始

3. 长度

let arry1 = ["1",1,[3,"5"]]
console.log(arry1.length)

4. 遍历数组

1. for 循环遍历

const arry2 = [1, "2", 3, 4, 5, true]
for (let i = 0; i < arry2.length; i++) {
    console.log(arry2[i])
}

const arry2 = [1, 11, 3, 22, 888, 0]
let max = 0
for (let i = 0; i < arry2.length; i++) {
    arry2[i] > max ? max = arry2[i] : max
}
console.log(max)

2. map 遍历

const arr = ["red","blue","green"]
const newArr = arr.map(function (ele,index) {  // 有返回值,返回新数组
    return ele + "颜色"
})

3. forEach 遍历

只遍历数组,没有返回值,相当于加强版的for循环

const arr = ["red","blue","green"]
const newArr = []
arr.forEach(function (ele,index) {    // 没有返回值,不用写return
    newArr[index] = ele + "颜色"
})
console.log(arr)
console.log(newArr)

4. 使用 reduce 遍历,求数组元素的和

  1. 如果没有起始值,则把数组的第一个元素的值作为上一次值
  2. 每一次循环,把返回值座位下一次循环的上一次值,没有初始值,reduce循环的次数是数组的长度-1
  3. 如果有起始值,则把起始值作为上一次值,有初始值,reduce循环的次数是数组的长度
// 返回累计处理的结果,通常用于求和等
const arr = [100, 60, 80, 800]
const result = arr.reduce(function (previousValue,currentValue,currentIndex,array) {
    // previousValue: 上一次的值
    // currentValue: 当前值
    // currentIndex: 当前索引
    // array: 循环的原数组
    
    return previousValue + currentValue
    
},0)   // 0为初始值,回调函数的运算完成后再 + 初始值,默认为0
console.log(result)

// 箭头函数的写法
const result2 = arr.reduce((previousValue, currentValue) => previousValue + currentValue,0)

如果是对象数组,必须设置初始值为0

const arr = [
    {name: "张三",salary:10000},
    {name: "赵四",salary:8000},
    {name: "王五",salary:3000},
    {name: "周六",salary:18000},
]
const res = arr.reduce((pre,cur) => pre + cur.salary,0)  // 上一次的值 + 对象的 salary属性
console.log(res)

5. 修改元素

const arry2 = [1, 11, 3, 22, 888, 0]
arry2[0] = 111   // 指定索引修改
console.log(arry2)

6. 新增元素

1. 末尾追加 --- push()

const arry2 = [1, 11, 3, 22, 888, 0]
is_pushed = arry2.push(99,55,66)   // 返回值为追加元素后的数组新的长度
console.log(is_pushed)  
console.log(arry2)

2. 头部插入 --- unshift()

const arry2 = [1, 11, 3, 22, 888, 0]
is_pushed = arry2.unshift(99,55,66)
console.log(is_pushed)  // 返回值为追加元素后的数组新的长度
console.log(arry2)

3. 添加或删除元素

const months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 1)  // 删除指定索引和指定数量的元素
console.log(months);

months.splice(1, 0,"Fed")  // 从索引1开始,参数2为0时,表示新增元素,添加的元素为Fed
console.log(months);

7. 删除元素

1. 尾部删除一个元素 --- pop()

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
poped = arr.pop()   // 返回值为删除掉的元素
console.log(poped)

2. 头部删除一个元素 --- shift()

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
shifted = arr.shift()  // 返回值为删除掉的元素
console.log(poped)

3. 指定位置和数量删除 --- splice()

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
spliced = arr.splice(3,3)  // 从第几个开始,删除几个,默认删除指定位置后的所有,返回值为删除掉的元素数组
console.log(spliced)
console.log(arr)

8. 转成字符串

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
const newStr = arr.join("_")  // 以_拼接数组中的每一个元素

9. 指定元素的索引

1. indexOf() 返回指定元素的索引或-1

不存在返回-1,存在返回第一个符合条件元素的索引

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
console.log(arr.indexOf(0))

2. findeIndex() 返回指定元素的索引或-1

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
const itemIndex = arr.findIndex(value => value === 77)

10. 指定元素是否符合条件

1. find() 是否有指定元素,返回元素本身或undefined

参数是一个回调函数,所有数组元素依次遍历执行该回调函数,value为数组中的每一个元素,index为数组中每一个元素的索引,可以通过逻辑来查找数组中的指定元素

如果找到了,会立刻结束循环,并返回第一个符合查找条件的元素,否则返回值undefined

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
const is_find = arr.find(value => value === 100)
console.log(is_find)

应用场景

const arr = [
    {
        brand: "小米",
        products: [
            {
                name: "小米手机",
                price: 1999
            },
            {
                name: "小米冰箱",
                price: 3999
            },
            {
                name: "小米电视",
                price: 2999
            }
        ]
    },
    {
        brand: "华为",
        products: [
            {
                name: "mate60",
                price: 7999
            },
            {
                name: "华为平板",
                price: 5999
            },
        ]
    },
    {
        brand: "iphone",
        products: [
            {
                name: "iphone15",
                price: 8999
            },
            {
                name: "ipad",
                price: 6999
            },
        ]
    },
]

// 只找小米品牌的产品
const mi = arr.find(value => value.brand === "小米")
console.log(mi)

2. every() 是否全部符合条件,返回布尔值

检测数组所有元素是否都符合指定条件,如果所有的元素都通过检测,返回true,只要有一个不符合条件立刻结束函数执行,并返回false

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
const result = arr.every(value => value >= 10)  // 检查数组中的所有元素是否都大于等于10,如果有一个不符合条件,就返回false
console.log(result)

3. some() 是否包含指定元素,返回布尔值

some方法同样用于检测是否有满足条件的元素,如果有,则不继续检索后面的元素,直接返回true,如果都不符合,则返回一个false。

用法与find相似,只是find是返回满足条件的元素,some返回的是一个Boolean值,从语义化来说,是否包含更贴切。

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
const is_find = arr.some(ele => ele === 0)
console.log(is_find)

4. includes() 是否包含指定元素,返回布尔值

ES6新增的数组方法,用于检测数组是否包含某个元素,如果包含返回true,否则返回false,比较厉害的是,能直接检测NaN:

优点: 最简单的做法没有之一,不用回调,不用复杂的写法,一个方法直接搞定。

缺点: 是低版本浏览器支持不是很友好

const arr = [2, 0, 6, 1, 77, 0, 52, 0, 25, 7]
const is_find = arr.includes(0)
console.log(is_find)

11. 展开嵌套数组

递归展开不会影响原数组,所以需要重新赋值

const a = [
    [
        {
            "_value": "2024-03-13 11:01:42.460053",
            "_field": "stop_time",
        },
        {
            "_value": "2024-03-13 14:47:05.744634",
            "_field": "stop_time",
        },
        {
            "_value": "2024-03-13 10:42:50.809836",
            "_field": "stop_time",
        },
    ],
    [
        {
            "_value": "1224",
            "_field": "this_run_time",
        },
        {
            "_value": "14747",
            "_field": "this_run_time",
        },
        {
            "_value": "92",
            "_field": "this_run_time",
        },
    ],
    [
        {
            "_value": "2024-03-13 11:01:32.213785",
            "_field": "start_time",
        },
        {
            "_value": "2024-03-13 14:10:32.370990",
            "_field": "start_time",
        },
        {
            "_value": "2024-03-13 15:13:42.402375",
            "_field": "start_time",
        },
    ]
]
const result = a.flat()
console.log(result)

// 手动循环
const result = []
for (let i = 0;i<a.length;i++){
    for (let y = 0;y<a[i].length;y++){
        result.push(a[i][y])
    }
}
console.log(result)

12. 求数组最大值

const arr = [1,2,3,4]
Math.max(...arr)   // Math.max(1,2,3)

13. 合并数组

const arr1 = [1,2,3,4]
const arr2 = [5,6,7,8]
const arr3 = [...arr1,...arr2]
console.log(arr3)

const newArr = arr1.concat(arr2)   // 返回新数组
console.log(newArr)

14. 数组解构

// 底层会根据位置批量的将数组中的值赋值给变量
const [max, min, avg] = [100,60,80]
console.log(max)
console.log(min)
console.log(avg)

交换两个变量的值

let a = 1
let b = 2
console.log(a)
console.log(b)  
;[a,b] = [b,a]  // 解构赋值语法前面必须加分号,否则报错
console.log(a)
console.log(b)

let a = 1
let b = 2  
;[a,b] = [b,a]  // 解构赋值语法前面必须加分号,否则报错
console.log(a)
console.log(b)

// 还有立即执行函数的后面必须加分号
(function(){ })();

// 以数组的字面量开头,并且上面有代码时
const name = "123";
[1,2,3].map(function(item){
    console.log(item)
})

元素少,变量名多

const [a, b, c, d, e] = [100,60,80,800]  // 多出来的变量没有被赋值,所以e的值是undefined

使用默认参数解决,值为undefined的情况

const [a = 0, b = 0, c = 0, d = 0, e = 0] = [100,60,80,800]

元素多,变量名少

const [a, b] = [100,60,80,800]   // 多出来的变量不会再赋值

使用剩余函数解决上面的问题

const [a, b, ...args] = [100,60,80,800]

按需导入赋值, 忽略某些值

const [a, , , d] = [100,60,80,800]  // 不给变量名,用以忽略60和80 

解构 多维数组

// 解构一维数组
const [a, b, c] = [100, 60, [80,800]]

// 解构二维数组
const [a, b, [c, d]] = [100, 60, [80,800]]

15. 筛选数组

筛选数组符合条件的元素,并返回满足条件的元素组成的新数组

const arr = [100, 60, 80, 800]

// 匿名函数筛选小于等于80的元素
const newArr = arr.filter(function (item, index) {
    return item <= 80
})
console.log(newArr)

// 匿名函数筛选大于等于100的元素
const newArr2 = arr.filter(item => item >= 100)
console.log(newArr2)

16. 求数组中元素的和

1. 计算原理

  1. 如果没有起始值,则把数组的第一个元素的值作为上一次值
  2. 每一次循环,把返回值座位下一次循环的上一次值,没有初始值,reduce循环的次数是数组的长度-1
  3. 如果有起始值,则把起始值作为上一次值,有初始值,reduce循环的次数是数组的长度

2. 基本数组计算

// 返回累计处理的结果,通常用于求和等
const arr = [100, 60, 80, 800]
const result = arr.reduce(function (previousValue,currentValue,currentIndex,array) {
    // previousValue: 上一次的值
    // currentValue: 当前值
    // currentIndex: 当前索引
    // array: 循环的原数组
    
    return previousValue + currentValue
    
},0)   // 0为初始值,回调函数的运算完成后再 + 初始值,默认为0
console.log(result)

// 箭头函数的写法
const result2 = arr.reduce((previousValue, currentValue) => previousValue + currentValue,0)

3. 如果是对象数组,必须设置初始值为0

const arr = [
    {name: "张三",salary:10000},
    {name: "赵四",salary:8000},
    {name: "王五",salary:3000},
    {name: "周六",salary:18000},
]
const res = arr.reduce((pre,cur) => pre + cur.salary,0)  // 上一次的值 + 对象的 salary属性
console.log(res)

17. 排序

const arr = [2, 6, 1, 77, 52, 0, 25, 7]
const result = arr.sort()   // 会影响原数组,是在原数组的基础上做的排序
console.log(arr)
console.log(result)

18. 反转

const arr = [2, 6, 1, 77, 52, 0, 25, 7]
arr.reverse()       // 会影响原数组,是在原数组的基础上做的排序
console.log(arr)

8.3 对象

无序的数据集合

1. 声明

// 使用对象字面量声明
const obj1 = {
    name:"小红",
    age:18,
    sex:"女"
}

// 使用构造函数声明对象
const obj2 = new Object({
    name:"小红",
    age:18,
    sex:"女"
})

2. 属性

const obj2 = new Object({
    属性名:属性值
})

3. 方法

const obj2 = new Object({
    方法名:函数
})

4. 新增

1. 新增属性

const obj1 = {
    name:"小红",
    age:18,
    sex:"女"
}

obj1.hobby = "烫头"

// 使用拷贝方式追加属性
Object.assign(obj1,{hobby: "烫头"})
console.log(obj1)

2. 新增方法

const obj1 = {
    name:"小红",
    age:18,
    sex:"女"
}

obj1.sayHello = function(){
    console.log("Hello")
}

obj1.sayHello()

5. 查询

const obj1 = {
    'user-name':"小红",  // 如果属性名中有中横线-,必须将属性名改为字符串
    age:18,
    sex:"女"
}

obj1.age
obj1["age"]
obj1["user-name"]

6. 修改

const obj1 = {
    name:"小红",
    age:18,
    sex:"女"
}

obj.name = "小明"

7. 删除

const obj1 = {
    name: "小红",
    age: 18,
    sex: "女",
    sayHello: function () {
        console.log("Hello")
    }
}

delete obj1.sayHello

8. 遍历对象

const obj1 = {
    'user-name': "小红",
    age: 18,
    sex: "女"
}

for (let key in obj1) {
    console.log(key," --- ",obj1[key])
}

数组嵌套对象的遍历

const obj1 = [
    {
        'user-name': "小蓝",
        age: 20,
        sex: "女"
    },
    {
        'user-name': "小红",
        age: 18,
        sex: "女"
    },
    {
        'user-name': "小绿",
        age: 25,
        sex: "男"
    },
]

for (let i = 0; i < obj1.length; i++) {
    console.log(obj1[i]["user-name"]);
    console.log(obj1[i].age);
}

9. 内置对象 --- Math

1. 属性

1. PI

圆周率π,约等于3.14159

Math.PI * (3 * 3)

2. 方法

1. random()

随机生成0-1之间的小数,包含0,不包含1

Math.random()

2. ceil()

向上取整的整数

Math.ceil(1.33)

3. floor()

向下取整的整数

Math.floor(1.33)

4. round()

四舍五入的整数

Math.round(1.5)

5. max()

最大值

Math.max(1,2,3,5,6,7,8,9,0,-1)

6. min()

最小值

Math.min(1,2,3,5,6,7,8,9,0,-1)

7. pow()

幂运算

Math.pow(10,6)  // 10的6次方

8. abs()

绝对值

Math.abs(-22)

9. 0-10之间的随机数,包含0和10

Math.floor(Math.random() * (10 + 1))

10. 随机抽取数组中的一个元素

let arr = ["red","green","blue"]
arr[Math.floor(Math.random() * arr.length)]

11. 随机抽取N-M之间的数

function getRandom(N, M) {
    return Math.floor(Math.random() * (M - N + 1)) + N
}

console.log(getRandom(0,2));  // 0-2 之间的整数

12. 案例

1. 随机显示数组中的一个名字到页面中,并且不能重复

let personList = ["赵云", "黄忠", "关羽", "张飞", "马超", "刘备", "曹操"]
let personList2 = personList
for (let i = 0; i < personList.length; i++) {
    randomInt = Math.floor(Math.random() * personList2.length)
    document.write(personList2.splice(randomInt, 1));
}

2. 程序随机生成一个1-10之间的数字,用户随便输入数字,系统提示猜大了还是猜小了,如果猜对了,提醒猜对了,并退出程序

randomInt = Math.floor(Math.random() * 11)
let count = 3

while (count) {
    inputInt = +prompt("请输入数字")
    if (inputInt > randomInt) {
        alert("猜大了")
    } else if (inputInt < randomInt) {
        alert("猜小了")
    } else {
        alert("猜对了")
        break
    }
    count--
    if (!count){
        alert("次数已用完")
    }
}

3. 颜色随机生成,使用参数来确定颜色是十六进制还是rgb格式

function randomColor(is_hex) {
    // 十六进制
    if (is_hex) {
        let hex_str = "#"
        let hex_arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]
        for (let i = 0; i < 6; i++) {
            randomStr = Math.floor(Math.random() * hex_arr.length)
            hex_str += hex_arr[randomStr]
        }
        document.write(`<div style='width: 200px;height: 200px;background-color: ${hex_str}'></div>`)

    } else {
        r = Math.floor(Math.random() * 256)
        g = Math.floor(Math.random() * 256)
        b = Math.floor(Math.random() * 256)
        document.write(`<div style='width: 200px;height: 200px;background-color: rgb(${r},${g},${b})'></div>`)
    }
}

randomColor(1)

10. 对象解构

基本解构

const {name,age} = {   // 变量名必须和对象中的属性名一致,且不要和全局变量名冲突,如果不一致,变量名默认为undefined
    name: "小明";
    age: 18
}

const { name } = {
    name: "小明";
    age: 18
}

可以重新赋值来修改变量名

const {name: userName,age} = {   // 旧变量名: 新变量名
    name: "小明";
    age: 18
}

console(userName)

解构数组对象

const [person1,person2] = [
    {
        name: "小明";
    	age: 18
    },
    {
        name: "小红";
    	age: 16
    }
]
console.log(person1,person2)


const [{name: name1,age: age1},{name: name2,age: age2}] = [
    {
        name: "小明";
    	age: 18
    },
    {
        name: "小红";
    	age: 16
    }
]

console.log(name1,age1)
console.log(name2,age2)

多级对象

const {name, family: {mother, father}} = {  // 需要声明要解构的里层对象名
    name: "小明",
    family: {
        mother: "大慧",
        father: "大勇"
    },
    age: 6
}

多级数组,多级对象

const [{name, family: {mother, father}}] = [  // 需要声明要解构的里层对象名
        {  
            name: "小明",
            family: {
                mother: "大慧",
                father: "大勇"
            },
            age: 6
        }
]

11. 获取所有属性名

const obj = { name: "佩奇", age: 6}

// 获取对象中的所有属性名,返回的是一个数组
const arr = Object.keys(obj)  

12. 获取所有属性值

const obj = { name: "佩奇", age: 6}

// 获取对象中的所有属性值,返回的是一个数组
const arr = Object.values(obj)

13. 对象深拷贝

const obj = { name: "佩奇", age: 6}
const newObj = {}

const result = Object.assign(newObj,obj)  // 返回值是深拷贝后的新对象
console.log(result);
console.log(newObj)

9. 数据类型操作

9.1 检测数据类型

let userName = "刘德华"
let age = 18

let is_manger = false

console.log(typeof userName)  // 当做运算符使用, typeof 变量名
console.log(typeof(age))      // 当做函数调用, typeof(变量名)

9.2 类型转换

1. 隐式转换

某些运算符执行时,系统内部自动将数据类型进行转换

  1. + 号左右两边只要有一个是字符串,程序执行的时候都会将另一个转成字符串,然后再进行运算

  2. 除了+ 以外,比如 - * /等都会将数据转成数字类型,然后再进行运算

  3. console.log(+'123') 这里的+号,在程序执行的时候,会将 '123'这个字符串转成数字 123

    let num = +'123'
    

隐式转换: 转换类型不明确,要靠经验才能总结出来

2. 显式转换

1. 字符串转数字

let num1 = "123"
Number(num1)

// 只保留字符串中的整数
let width = "200px"
parseInt(width)  // 200

let width = "165.23px"
parseInt(width)  // 165

let width = "165.23px"
parseFloat(width) // 165.23

let width = "a165.23px"
parseFloat(width) // NaN,字符串中必须以数字开头

2. 转数字

let num1 = true
Number(num1)  	// 1

let num2 = false
Number(num1)    // 0

let str1 = ""
Number(str1)   // 0

let con1 = null
Number(con1)  // 0

let con2 = undefined
console.log(Number(con2));   // NaN

3. 转为布尔

''(空字符串)、0、undefined、null、false、NaN转成布尔值后都是false,其余则为true

10. 数据类型原理

10.1 值类型和引用类型

1. 值类型

简单数据类型/基本数据类型: 在存储时,在变量中存储的是值本身,因此叫做值类型,如:string、number、boolean、undefined、null

2. 引用类型

复杂数据类型: 在存储时,变量中存储的是地址(引用),因此叫做引用类型,如: array、object、date

10.2 内存分配原则

1. 堆 (操作系统)

用以存储复杂类型(对象),一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收,先入后出

2. 栈 (操作系统)

用以存储简单数据类型,由操作系统自动分配释放存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈,先入先出

11. 函数

11.1 声明

function sayHello() {
    document.write("Hello World")
}

11.2 调用

sayHello()

11.3 参数

1. 位置参数

function numCount(start,end) {   // 位置参数声明
    let count = 0
    for (let i = start; i <= end; i++) {
        count += i
    }
    console.log(count)
}

numCount(1,100)   // 位置传参

2. 默认参数

function numCount(start=1,end=100) {  // 默认参数声明
    let count = 0
    for (let i = start; i <= end; i++) {
        count += i
    }
    console.log(count)
}

numCount(20,50)   // 如果有参数被传递,优先使用传递的参数
numCount()  // 如果没有,使用默认参数

3. 动态参数

不确定传递多少个实参

function numCount() {
    let count = 0
    for (let i = 0; i <= arguments.length; i++) {   // arguments就是动态参数,通过函数传参到函数中的所有参数会保存在arguments中,只存在于函数内部,是个伪数组,无法使用push()、pop()等方法
        count += arguments[i]
    }
    console.log(count)
}
numCount(1,2,3,523,12,5432,5)

4. 剩余参数(推荐)

不确定传递多少个实参

function numCount(a,b,...args) {  // args就是剩余参数,通过...将传递的参数聚合成一个数组
    let count = 0
    count += a
    count += b
    for (let i = 0; i <= args.length; i++) {   
        count += args[i]
    }
    console.log(count)
}
numCount(1,2,3,523,12,5432,5)

5. 展开运算符

const arr = [1,2,3,4]
console.log(...arr)   // 1 2 3 4

不会修改原数组,一般在实参中使用

// 求数组的最大值
const arr = [1,2,3,4]
Math.max(...arr)   // Math.max(1,2,3)

// 合并数组
const arr1 = [1,2,3,4]
const arr2 = [5,6,7,8]
const arr3 = [...arr1,...arr2]

11.4 返回值

function num() {
    return 20
}
result = num()
console.log(result)

11.5 作用域

1. 全局作用域

作用于所有代码执行的环境(整个script标签内部)或者一个独立的js文件,就是全局作用域,在全局作用域中声明的变量,任何其他作用域都可以被访问

const num = 10 
function fn(){
    console.log(num)
}

注意点:

  1. 为 window 对象动态添加的属性默认也是全局的,不推荐
  2. 函数中为使用任何关键字声明的变量为全局变量,不推荐
  3. 尽可能少的声明全局变量,防止全局变量被污染

2. 局部作用域 -- 函数作用域

在函数内部声明的变量只能在函数内部被访问,外部无法直接访问,称为函数作用域

function getSum(){
    // 函数作用域中声明的变量为局部变量
    const num = 10
}
console.log(num)  // 报错,函数外部不能使用局部作用域声明的变量

3. 局部作用域 -- 块作用域

在JavaScript 中使用{}包裹的代码成为代码块,代码块内部声明的变量外部 有可能 无法被访问

for (let i=1;i<=3;i++){
    console.log(i)
}
console.log(i)  // 报错

if (true){
    const i = 10
}
console.log(i)  // 报错

注意点:

  1. let 声明的变量会产生块作用域,var声明的不会产生块作用域
  2. const 声明的常量也会产生块作用域
  3. 不同代码块之间的变量无法相互访问
  4. 推荐使用 let 或const,在代码块中声明变量

3. 变量的特殊情况

如果函数内部,变量没有声明,直接赋值,也当做全局变量,作用域为全局作用域,但是强烈不推荐

4. 作用域链

作用域链的本质上是底层的变量查找机制

  1. 在函数被执行时,会优先查找当前函数作用域中查找变量
  2. 如果当前作用域查找不到则会一次逐级查找父级作用域直到全局作用域
  3. 子作用域能够访问父作用域,父作用域无法访问子作用域

11.6 匿名函数

let funA = function (x,y) {  // 没有函数名的,就是匿名函数
    console.log(x + y)
}
funA(1,2) 

// 立即执行匿名函数
// 写法1
(function () {
    console.log(1)
})()

// 写法2
(function () {
    console.log(1)
}())


// 两个相邻的匿名函数之间必须加分号隔开,否则报错
(function () {
    console.log(1)
})();

(function () {
    console.log(2)
}())

11.7 逻辑中断

为了防止未传参数,导致报错

function fn(x,y) {
    x = x || 0   // x,y未传值时为undefined,undefined + undefined 会报错
    y = y || 0
    console.log(x + y)
}
fn()

11.8 闭包

在嵌套函数中,里层函数引用外层函数中,且不是全局的变量,就称之为闭包,和pyhton 不一样的是 python无法修改闭包内的变量,JS可以

闭包是有内存泄漏的问题

function warpper(){
    let a = 1
    function inner(){
        a++
        console.log(a)
    }
    return inner
}

const func = warpper()
func()

11.9 箭头函数

引入箭头函数的目的是更剪短的写法并且不绑定this,箭头函数的语法比函数表达式更简洁,箭头函数更适用于那些使用匿名函数的地方

1. 基本语法

// 匿名函数
const fn = function(){
    console.log(1)
}
fn()

// 箭头函数
const fn = () =>{
    console.log(1)
}
fn()

// 带参数的箭头函数
const fn = (a) =>{
    console.log(a)
}
fn()

如果箭头函数只有一个参数,可以简写为下面形式

const fn = a =>{
    console.log(a)
}
fn(1)

如果函数的体只有一行代码时,可以简写成以下形式

const fn = a => console.log(a)
fn(1)

如果函数体只有一行代码时,并且需要return,可以省略return

const fn = a =>{
    return a + 1
}
fn(1)

// 简写,省略return
const fn = (x, y) => x +y
fn(1,3)

直接返回一个对象的字面量,但是需要使用小括号包起来

const fn = (username) => ({username:username})
fn("小明")

// 解构后的简写
const fn = (username) => ({username})
fn("小明")

2. 参数

位置参数

const fn = (a,b) => {
    console.log(a,b)
}
fn(1,2)

剩余参数

const getSum = (...args) => {
    let sum = 0
    for (let i=1;i<args.length;i++){
        sum += args[i]
    }
    return sum
}
getSum(1,2)

3. this 的指向

全局函数

function fn(){
    console.log(this) // 此时this的指向是window对象
}
window.fn()  // JS中声明的函数,默认都是挂载在window对象上的,所以默认是window对象调用函数,可以省略

对象中的方法

const obj = {
    name: "andy",
    sayHi: function(){
        console.log(this)   // 此时this的指向是obj对象
    }
}
obj.sayHi()

箭头函数的this,不会创建自己的this,只会沿用上一层作用域的this

const fn = () => console.log(this)   // 上一层作用域的this指向的是全局作用域,所以箭头函数中的this就是window对象
fn()


const obj = {
    name: "andy",
    sayHi: () => console.log(this)   // 上一层作用域的this指向的是全局作用域,此时箭头函数中的this就是window对象
}
obj.sayHi()


const obj = {
    name: "andy",
    sayHi: function(){
        const fn = () => console.log(this)   // 上一层作用域的this指向的obj对象,所以箭头函数中的this就是obj对象
    }
}
obj.sayHi()

DOM事件中的回调函数,不推荐使用箭头函数,因为this指向的是window对象

13. 正则表达式

13.1 定义

const 变量名 = /表达式/   // 其中 / / 是正则表达式的字面量

13.2 规则字符

1. 边界符

用来提示字符所在的位置

1. ^

匹配行首的文本(以谁开始),精准匹配

const regObj = /^a/
const result = regObj.test("asdf小2名")

2. $

匹配行尾的文本(以谁结束),精准匹配

const regObj = /名$/
const result = regObj.test("asdf小2名")

2. 量词

设定某个模式出现的次数

**1. ***

重复零次或更多次

const regObj = /^哈*$/   // 以哈开始,以哈结尾,零次或多次
const result = regObj.test("哈哈哈")

2. +

重复一次或更多次

const regObj = /^哈+$/    // 以哈开始,以哈结尾,一次或多次
const result = regObj.test("哈哈哈")

3. ?

重复零次或一次

const regObj = /^哈?$/   // 以哈开始,以哈结尾,零次或一次
const result = regObj.test("哈哈哈")

4. {n}

重复n次

const regObj = /^哈{1}$/   // 以哈开始,以哈结尾,一次
const result = regObj.test("哈哈哈")

5. {n,}

重复n次或更多次,逗号左右不允许有空格

const regObj = /^哈{1,}$/   // 以哈开始,以哈结尾,一次或更多次
const result = regObj.test("哈哈哈")

6. {n,m}

重复n次到m次,逗号左右不允许有空格

const regObj = /^哈{1,4}$/   // 以哈开始,以哈结尾,一次到四次
const result = regObj.test("哈哈哈")

3. 字符类

1. []

匹配字符集合中的任意一个字符,只选一个

const regObj1 = /[abc]/   
const regObj2 = /[a-z]/   // a-z的英文字母  
const regObj3 = /[A-Z]/   // A-Z的英文字母  
const regObj4 = /[0-9]/   // A-Z的英文字母 
const regObj4 = /[a-zA-Z0-9]/   // 下小写英文字母和数字  
const result = regObj.test("andy")

2. []中的^

在中括号里的^表示取反符号

const regObj1 = /[^a-z]/  // 除a-z的所有字符

3. .

除了换行符之外的任何单个字符

4. 预定义类


预定义类 说明
\d 匹配0-9之间的任一数字,相当于[0-9]
\D 匹配所有0-9以外的字符,相当于[^0-9]
\w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9]
\W 匹配除字母、数字和下划线以外的字符,相当于[^A-Za-z0-9]
\s 匹配空格(包括换行符、制表符、空格符等),相当于[\t\r\n\v\f]
\W 匹配非空格的字符,相当于[^\t\r\n\v\f]

5. 修饰符

约束正则执行的某些细节行为,如是否区分大小写、是否支持多行匹配等


修饰符 说明
i ignore的缩写,正则匹配时字母不区分大小写
g global的缩写,匹配所有满足正则表达式的结果
const regObj = /a/i   // 修饰符卸载正则后面即可
regObj.test("a")  // true
regObj.test("A")  // true

13.3 常用方法

1. 是否匹配

const regObj = /\d+/
regObj.test("1asdf小2名")  // 返回布尔值

13.4 常用匹配规则

1. QQ号

/^[1-9][0-9]{4,}$/   // 从10000开始

2. 用户名

// 大小写英文字符、数字、下划线、短横线组成,并且用户名长度在6-16位
/^[a-zA-Z0-9-_]{6-16}$/

3. 日期格式

/^\d{4}-\d{1,2}-\d{1,2}$/

4. 手机号

/^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$/

5. 验证码

/^\d{6}$/

6. 密码

/^[a-zA-Z0-9-_]{6-20}$/

14. 面试题

1. 垃圾回收机制(GC)

JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收

1. 变量、对象、函数在内存中的生命周期

  1. 内存分配: 声明变量、函数、对象的时候,系统会自动为它们分配内存
  2. 内存使用: 即读写内存,也就是使用变量、调用函数等
  3. 内存回收: 使用完毕,由垃圾回收期自动回收不再使用的内存

2. 注意

  1. 全局变量一般不会回收(关闭页面回收)
  2. 一般情况下局部变量的值不用了会被自动回收

3. 内存泄露

程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄露

4. 如何回收?

1. 堆栈空间分配区别

  1. 栈(操作系统): 由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面
  2. 堆(操作系统): 一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收. 复杂数据类型放在堆里面

2. 垃圾回收算法: 引用计数法、标记清除法

  1. 引用计数(IE采用的是引用计数算法,定义内存不再使用,就是看一个对象是否有指向它的引用,没有了引用就回收对象)
    1. 跟踪记录被引用的次数
    2. 如果被引用了一次,即记录次数+1
    3. 如果减少了一次引用,即记录次数-1
    4. 如果引用次数为0,则释放内存

但是引用计数有一个致命的问题: 嵌套引用(循环引用),即如果两个对象相互引用,尽管它们已经不再使用,垃圾回收器也不会进行回收,导致内存泄露

function fn(){
    let o1 = {}
    let o2 = {}
    o1.a = o2
    o2.a = o1
}
fn()  // o1和o2的引用次数永远不会是0,这样的相互引用如果大量存在就会导致大量的内存泄漏
  1. 标记清除(标记清除算法将"不再使用的对象"定义为"无法到达的对象")
    1. 从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,都是还需要使用的
    2. 哪些无法由根部触发触及到的对象被标记为不再使用,稍后进行回收

2. 变量提升

是 JS 的设计缺陷

  1. 把所有使用var关键字声明的变量提升到 当前作用域 的最前面一行,let或const声明的变量没有变量提升的问题
  2. 只提升声明操作,不提升赋值操作
console.log(num + "件")
var num = 10

// 转成真实逻辑代码为:
var num 
console.log(num + "件")  // 此时的num为undefined
num = 10

3. 函数提升

  1. 会把所有函数的生命提升到当前作用域的最前面一行
  2. 只提升函数声明,不提升函数调用
fn()
function fn(){
    console.log("函数提升")
}

// 相当于
function fn(){
    console.log("函数提升")
}
fn()


// 函数表达式必须先声明赋值,再调用,否则会报错
fun()   // 这里报错
var fun = function (){
    console.log("函数表达式")
}

// 相当于
var fun
fun()   // 这里fun是undefined,报错
fun = function (){
    console.log("函数表达式")
}

15. 面向对象

12.1 构造函数

一种特殊的函数,用来初始化对象

1. 创建对象的三种方式

1. 使用字面量创建对象

const obj = {
    name: "小明",
    age: 18
}

2. 使用 new Object 创建对象

const obj = new Object({
    name: "小明",
    age: 18
})

3. 利用构造函数创建对象

// 将一些对象的公共部分凡在函数中,这个函数就是构造函数,可以通过构造函数快速创建多个类似的对象
function Pig(name,age,gender){
    this.name = name
    this.age = age
    this.gender = gender
}

// 使用构造函数创建对象
const Peppa = new Pig("佩奇",6,"女")
const George = new Pig("乔治",3,"男")

约定

  1. 构造函数的函数名以大写字母开头
  2. 只能通过 new 来实例化对象

2. 实例成员

实例对象中的属性和方法称为实例成员(实例属性和实例方法)

  1. 为构造函数传入参数,创建机构相同但值不同的对象
  2. 构造函数创建的实例对象彼此独立互不影响(内存地址不同)
// 构造函数
function Person(name,age){
    // 构造函数内部的 this 就是实例对象
    
    // 实例属性
    this.name = name
    
    // 实例方法
    this.sayHi = function (){
        console.log("大家好~")
    }
}

const p1 = new Person("小明",18)
console.log(p1.name)   // 访问实例属性
p1.sayHi()    // 调用实例方法

3. 静态成员

构造函数的属性和方法被称为静态成员(静态属性和静态方法)

  1. 静态成员只能构造函数来访问
  2. 静态方法中的this指向构造函数
// 构造函数
function Person(name,age){
    // 省略实例成员
}

// 静态属性
Person.eyes = 2
Person.arms = 2

// 静态方法
Person.walk = function(){
    console.log("人会走路")
    console.log("人有眼睛: ",this.eyes)
}

console.log(Pig.eyes)  // 访问静态属性
Person.walk()   // 访问静态方法

4. 存在的问题

构造函数中声明的复杂数据类型存在浪费内存的方式,用原型解决

12.2 内置构造函数

js中的基本数据类型: 字符串,数字等,在js的底层是将其包装成了复杂数据类型,所以可以使用 str.length 这个属性

1. Object

下面是常用的静态方法(只有构造函数Object可以调用)

// 使用构造函数创建对象
const obj ={ name: "佩奇", age: 6}

// 获取对象中的所有属性名,返回的是一个数组
const arr = Object.keys(obj)  
const arr2 = Object.values(obj)  
const arr2 = Object.assign(obj,{hobby: "烫头"})   // 使用拷贝方式追加属性

2. Array

3. String

4. Number

12.3 原型对象

1. 什么是原型对象

  1. 构造函数通过原型分配的函数是所有对象所共享的
  2. JS规定: 每一个构造函数都有一个 prototype 属性,指向另一个对象,这个对象称为原型对象
  3. 这个对象可以挂载函数,对象实例化不会多次创建原型上的函数,这样就可以节省内存
  4. 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法了
  5. 构造函数和原型对象中的 this 都指向 实例化对象
  6. 公共的属性写在构造函数中,公共的方法写在原型对象中

2. 给数组扩展方法

// 在原型对象上绑定max方法,保证每个数组都能使用这个方法
Array.prototype.max = function(){
    return Math.max(...this)
}

const arr = [1,2,3]
arr.max()

3. constructor属性

每个原型对象中都有 constructor 属性,该属性指向该原型对象的构造函数

function Star(){
}
// 方式一: 使用追加的方式给原型对象绑定方法
Star.say = function(){
    console.log("说话")
}
// 方式二: 使用对象统一创建公共方法
Star.prototype = {
    constructor: Star,  // 必须将 constructor 重新指向构造函数,否则找不到原型对象身上初始的那些方法了
    sing: function(){
        console.log("唱歌")
    },
    dance: function(){
        console.log("跳舞")
    },
}

const ldh = new Star()

12.4 对象原型

在每一个对象中都会有一个属性 __proto__ 指向构造函数的 prototype 原型对象,之所以实例化的对象可以使用构造函数 prototype 原型对象中定义的属性和方法,就是因为对象有 __proto__ 原型的存在

注意

  1. __proto__ 是JS非标准属性,只读的无法修改
  2. [[prototype]] 和 __proto__意义相同
  3. 用来表明当前实例对象指向那个原型对象
  4. __proto__对象原型中也有一个 constructor 属性,指向创建该实例对象的构造函数

12.5 原型继承

// 公共属性, Person 构造函数
function Person () {
    this.eays = 2
    this.head = 1
}

// Woman继承Person的公共属性
function Woman(){
}
Woman.prototype = new Person()    // Woman构造函数,通过原型来继承Person的属性,由于是new的Person对象,所以内存地址不同,是不同的实例
Woman.prototype.constructor = Woman   // 指定原型对象的 constructor 为 Woman 构造函数

// Man继承Person的公共属性
function Man(){
}

Man.prototype = new Person()    // Man构造函数,通过原型来继承Person的属性,由于是new的Person对象,所以内存地址不同,是不同的实例
Man.prototype.constructor = Man   // 指定原型对象的 constructor 为 Man 构造函数

12.6 原型链(面试题)

image-20240523105341079

**理论: **

  1. 只要是对象就有__proto__属性指向它的原型对象,只要是原型对象就有constructor属性指向它的构造函数
  2. 原型链本质上就是一个查找规则,实例对象想要调用属性或方法时,应该如何去查找,先在当前实例对象的身上查找,
  3. 如果找不到自动通过实例对象.__proto__(构造函数.prototype)身上查找,
  4. 如果没找到则继续在实例对象.__proto__(构造函数.prototype).__proto__(构造函数.prototype)上查找,
  5. 依次类推一直找到Object为止(Object.prototype.__proto__指向的是null)
  6. __proto__对象原型的意义在于为对象成员查找机制提供一个方向,或者说是一套路线
  7. 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
function Person(){
     
 }
const ldh = new Person()

console.log(ldh instanceof Person)  // ldh是否是Person的实例
console.log(ldh instanceof Object)
console.log(ldh instanceof Array)

12.7 使用面向对象来封装组件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .modal {
            width: 200px;
            height: 100px;
            border: 1px solid black;
            margin: 200px auto;
        }

        .title i {
            float: right;
            margin-right: 20px;
        }
        .title i:hover{
            cursor: pointer;
        }
    </style>
</head>
<body>
<button class="delete">删除</button>
<button class="login">登录</button>
<script>
    // 1. 模态框构造函数
    function Modal(title = "", message = "") {
        this.title = title
        this.message = message
        // 1. 创建 modal 模态框的盒子模型
        // 1.1 创建最外层div标签,并给其一个modal的class类名
        this.modalBox = document.createElement("div")
        this.modalBox.className = "modal"
        // 1.2 在盒子模型中放入标题,和提示信息标签
        this.modalBox.innerHTML = `
            <div class="title">${this.title} <i>x</i></div>
            <div class="body">${this.message}</div>
        `
        console.log(this.modalBox)
    }

    // 2. 给Modal的原型对象挂载 open 方法,用于打开模态框
    Modal.prototype.open = function () {
        // 判断页面中是否有 modal 盒子模型
        const box = document.querySelector(".modal")
        box && box.remove()   // 如果有box,就remove(),如果没有就不执行remove()代码

        // 将模态框盒子添加到页面中
        document.body.append(this.modalBox)

        // 绑定关闭事件,必须用到箭头函数,确保this指向实例对象,而不是i标签的DOM对象
        this.modalBox.querySelector("i").addEventListener("click", () => {
            this.close()
        })
    }

    // 3. 给Modal的原型对象挂载 close 方法,用于关闭模态框
    Modal.prototype.close = function () {
        this.modalBox.remove()
    }

    // 绑定点击事件,使用模态框
    document.querySelector(".delete").addEventListener("click", function () {
        // 实例化对象
        const m1 = new Modal("温馨提示", "您没有操作权限")
        // 调用open方法,在页面中显示模态框
        m1.open()
    })
</script>
</body>
</html>

16. 深浅拷贝

16.1 浅拷贝

只拷贝对象或数组中的第一层值类型的值,如果其中嵌套了引用类型,则拷贝的是内存地址,这样嵌套的引用类型发生了改变原引用类型也会受到影响

1. 拷贝对象

使用 Object.assgin() 完成浅拷贝

1.只有值类型

const obj = {
    name: "小明",
    age: 18
}
const newObj1 = {}

Object.assign(newObj1,obj)

console.log(obj)      // {name: '小明', age: 18}
console.log(newObj1)  // {name: '小明', age: 18}

// 将新对象的age改为20
newObj1.age = 20      
console.log(obj)      // {name: '小明', age: 18}
console.log(newObj1)  // {name: '小明', age: 20}

2. 其中嵌套了引用类型

const obj = {
    name: "小明",
    age: 18,
    family: {
        father: "老明"
    }
}
const newObj1 = {}
Object.assign(newObj1, obj)
console.log(obj)                // {"name": "小明","age": 18,"family": {"father": "老明"}}
console.log(newObj1)            // {"name": "小明","age": 18,"family": {"father": "老明"}}

//  修改嵌套的引用类型的值,会影响原对象
newObj1.family.father = "老小"  
console.log(obj)                // {"name": "小明","age": 18,"family": {"father": "老小"}}
console.log(newObj1)            // {"name": "小明","age": 18,"family": {"father": "老小"}}

2. 使用 ... 展开运算符进行浅拷贝

1.只有值类型

const obj = {
	name: "小明",
	age: 18
}

const newObj2 = {...obj}

console.log(obj)      // {name: '小明', age: 18}
console.log(newObj2)  // {name: '小明', age: 18}

// 将新对象的age改为20
newObj1.age = 20     
console.log(obj)      // {name: '小明', age: 18}
console.log(newObj2)  // {name: '小明', age: 20}

2. 其中嵌套了引用类型

const obj = {
    name: "小明",
    age: 18,
    family: {
        father: "老明"
    }
}
const newObj2 = {...obj}

console.log(obj)                // {"name": "小明","age": 18,"family": {"father": "老明"}}
console.log(newObj1)            // {"name": "小明","age": 18,"family": {"father": "老明"}}

// 修改嵌套的引用类型的值,会影响原对象
newObj1.family.father = "老小"  
console.log(obj)                // {"name": "小明","age": 18,"family": {"father": "老小"}}
console.log(newObj1)            // {"name": "小明","age": 18,"family": {"father": "老小"}}

2. 拷贝数组

1. 使用 concat() 进行浅拷贝

1.只有值类型

const arr = [1,2,3]

const newArr = Array.prototype.concat(arr)

console.log(arr)   		// [1,2,3]
console.log(newArr)		// [1,2,3]
    
// 将新数组中的索引为0的元素改为5
newArr[0] = 5
console.log(arr)		// [1,2,3]
console.log(newArr)		// [5,2,3]

2. 其中嵌套了引用类型

const arr = [1,2,3,[4,5,6]]
const newArr = Array.prototype.concat(arr)
console.log(arr)		// [1,2,3,[4,5,6]]
console.log(newArr)		// [1,2,3,[4,5,6]]

// 将嵌套引用类型的索引为0的元素修改为7,会影响原数组
newArr[3][0] = 7
console.log(arr)		// [1,2,3,[7,5,6]]
console.log(newArr)		// [1,2,3,[7,5,6]]

2. 使用 ... 运算符 进行浅拷贝

1.只有值类型

const arr = [1,2,3]
const newArr = [...arr]

console.log(arr)   		// [1,2,3]
console.log(newArr)		// [1,2,3]
    
// 将新数组中的索引为0的元素改为5
newArr[0] = 5
console.log(arr)		// [1,2,3]
console.log(newArr)		// [5,2,3]

2. 其中嵌套了复杂数据类型

const arr = [1,2,3,[4,5,6]]
const newArr = [...arr]
console.log(arr)		// [1,2,3,[4,5,6]]
console.log(newArr)		// [1,2,3,[4,5,6]]

// 将嵌套引用类型的索引为0的元素修改为7,会影响原数组
newArr[3][0] = 7
console.log(arr)		// [1,2,3,[7,5,6]]
console.log(newArr)		// [1,2,3,[7,5,6]]

16.2 深拷贝

无论是值类型或引用类型,都会拷贝其中元素的值

1. 函数递归

const obj = {
    name: "小明",
    age: 18,
    hobby: ["篮球", "足球"],
    family: {
        father: "爸爸",
        mother: "妈妈"
    }
}
const newObj = {}

function deepCopy(oldObj, newObj) {
    for (let k in oldObj) {
        // 处理数组的深拷贝,必须先处理Array,因为Array也是属于Object,但是对象不属于Array
        if (oldObj[k] instanceof Array) {
            newObj[k] = []
            deepCopy(oldObj[k], newObj[k])
        } else if (oldObj[k] instanceof Object) { // 处理对象的深拷贝
            newObj[k] = {}
            deepCopy(oldObj[k], newObj[k])
        } else {   // 处理数值类型的深拷贝
            newObj[k] = oldObj[k]
        }
    }
}

deepCopy(obj, newObj)
newObj.hobby[0] = "乒乓球"
console.log(obj)
console.log(newObj)

// newObj.family.father = "后爸"
// console.log(obj)
// console.log(newObj)

能实现简单的深拷贝,但是无法解决嵌套对象中存放的是对象中定义的数组,这种相互引用的结构

2. lodash.js中的cloneDeep()方法

1. 引用lodash.js

1. 标签引入

<script src="lodash.min.js"></script>

2. npm下载

npm i -g npm
npm i --save lodash

使用cloneDeep()深拷贝

const obj = {
    name: "小明",
    age: 18,
    hobby: ["篮球", "足球"],
    family: {
        father: "爸爸",
        mother: "妈妈"
    }
}

const newObj = _.cloneDeep(obj)

3. JSON.stringify()

核心思想就是,将对象转成字符串后,就是值类型了,当字符串再发序列化成对象时,是要开辟新的内存空间的,内存地址不同

const obj = {
    name: "小明",
    age: 18,
    hobby: ["篮球", "足球"],
    family: {
        father: "爸爸",
        mother: "妈妈"
    }
}

const newObj = JSON.parse(JSON.stringify(obj))

newObj.hobby[0] = "乒乓球"
newObj.family.family = "后爸"
console.log(obj)
console.log(newObj)

17. 异常处理

17.1 抛异常

function counter(x, y) {
    if (!x || !y) {
        throw new Error("参数不能为空")  // 抛异常,终端整个程序的运行
    }
    return x + y
}
counter()

17.2 捕获异常

function counter(x, y) {
    if (!x || !y) {
        throw new Error("参数不能为空")  // 抛异常,终端整个程序的运行
    }
    return x + y
}


try{
    counter()
} catch (e) {
    console.log(e)
} finally {
    // 无论是否出错,都会执行 finally 中的代码,通常用于释放资源
}

17.3 断点调试(debugger)

function counter(x, y) {
    debugger   // 打开浏览器的检查,直接调到这里进行调试
    if (!x || !y) {
        throw new Error("参数不能为空")
    }
    return x + y
}

18. this的指向

18.1 普通函数的指向

1. 普通模式

谁调用我,this就指向谁

2. 严格模式

<script>
	'use strict'  // 开启严格模式
    function fn(){
        console.log(this)  // 严格模式情况下的普通函数中this的指向是undefined
    }
    fn()
</script>

18.2 箭头函数的指向

箭头函数中是不存在this的,箭头函数会默认绑定最近一级作用域的this的值,如果没有找到会一层一层的查找this,直至有this定义的作用域

DOM对象的回调函数和原型对象的方法中最好不要用箭头函数

18.3 改变普通函数的 this 指向

1. call()

调用函数,同时也可以改变this的指向

const obj = {
    userName: "pick"
}

function fn(x, y) {
    console.log(this)
    console.log(this.userName)
    console.log(x, y)
    console.log(x + y)
}

// 调用函数,并改变this的指向
// 参数1: 函数运行时,this的指向
// 参数2...: 可以传多个参数
fn.call(obj, 1, 2)  // 将fn函数的this指向obj对象

2. apply()

调用函数,同时也可以改变this的指向

const obj = {
    userName: "pick"
}

function fn1(x,y) {  // 位置参数,和数组的长度一一对应
    console.log(this)
    console.log(this.userName)
    console.log(arr)
}

function fn2(...arr) {  // 可以使用 ... 来聚合参数,得到一个数组
    console.log(this)
    console.log(this.userName)
    console.log(arr)
}

// 调用函数,并改变this的指向
// 参数1: 函数运行时,this的指向
// 参数2:
fn1.apply(obj, [1, 2])    // 将fn函数的this指向obj对象
fn2.apply(obj, [1, 2])    // 将fn函数的this指向obj对象

应用场景:

const arr = [1,2,3]
const res = Math.max(...arr)  		   // 展开运算符,求数组最大值
const res = Math.max.apply(Math,arr)   // apply(),求数组最大值

3. bind()

不调用函数,但是可以改变this的指向

const obj = {
    userName: "pick"
}

function fn(x, y) {  // 必须使用... 来聚合参数
    console.log(this)
    console.log(this.userName)
    console.log(x, y)
    console.log(x + y)
}

// 调用函数,并改变this的指向
// 参数1: 函数运行时,this的指向
// 多个参数...
// 返回值是更改过this指向的新的函数
const newFn = fn.bind(obj, 1, 2)
newFn()   // 必须手动调用新的函数

应用场景: 有一个按钮,按下的时候立刻禁用,2秒后关闭禁用

方式一

<button>发送短信</button>

<script>
    const btn = document.querySelector("button")
    btn.addEventListener("click", function () {
        this.disabled = true
        setTimeout(function () {
            btn.disabled = false
        }, 2000)
    })
</script>

方式二

<button>发送短信</button>
<script>
    const btn = document.querySelector("button")
    btn.addEventListener("click", function () {
        this.disabled = true
        setTimeout(() => {
            this.disabled = false   // 如果想在里面使用this,必须使用箭头函数,找外层作用域中的指向btn的this
        }, 2000)
    })
</script>

方式三

<button>发送短信</button>
<script>
    document.querySelector("button").addEventListener("click", function () {
        this.disabled = true
        setTimeout(function () {
            this.disabled = false
        }.bind(this), 2000)   // 使用bind(),改变setTimeout()中回调函数的this的指向,这里不需要必须调用,2s后自动调用,所以不能使用 call() 或 apply()
    })
</script>
posted @ 2024-04-22 17:21  河图s  阅读(33)  评论(0)    收藏  举报