JavaScript笔记


JavaScript 笔记

1. 基础

1.1 介绍

1.1.1 引入方式

JavaScript 程序不能独立运行,它需要被嵌入 HTML 中,然后浏览器才能执行 JavaScript 代码。通过 script 标签将 JavaScript 代码引入到 HTML 中,有两种方式:

  1. 内部方式

    通过 script 标签包裹 JavaScript 代码:

    <body>
      <!-- 内联形式:通过 script 标签包裹 JavaScript 代码 -->
      <script>
        alert('嗨,欢迎来传智播学习前端技术!')
      </script>
    </body>
    
  2. 外部方式

    一般将 JavaScript 代码写在独立的以 .js 结尾的文件中,然后通过 script 标签的 src 属性引入

    // demo.js
    document.write('嗨,欢迎来传智播学习前端技术!')
    
    <body>
      <!-- 外部形式:通过 script 的 src 属性引入独立的 .js 文件 -->
      <script src="demo.js"></script>
    </body>
    

    如果 script 标签使用 src 属性引入了某 .js 文件,那么标签的代码会被忽略!!!如下代码所示:

    <body>
      <!-- 外部形式:通过 script 的 src 属性引入独立的 .js 文件 -->
      <script src="demo.js">
        // 此处的代码会被忽略掉!!!!
          alert(666);  
      </script>
    </body>
    

1.1.2 注释和结束符

通过注释可以屏蔽代码被执行或者添加备注信息,JavaScript 支持两种形式注释语法:

  1. 单行注释

    使用 // 注释单行代码:

    <body>
      
      <script>
        // 这种是单行注释的语法
        // 一次只能注释一行
        // 可以重复注释
        document.write('嗨,欢迎来传智播学习前端技术!');
      </script>
    </body>
    
  2. 多行注释

    使用 /* */ 注释多行代码

    <body>
      
      <script>
        /* 这种的是多行注释的语法 */
        /*
            更常见的多行注释是这种写法
            在些可以任意换行
            多少行都可以
          */
        document.write('嗨,欢迎来传智播学习前端技术!')
      </script>
    </body>
    

    注:编辑器中单行注释的快捷键为 ctrl + /

1.1.3 结束符

在 JavaScript 中 ; 代表一段代码的结束,多数情况下可以省略 ; 使用回车(enter)替代

这里和其他传统的编程语言有点区别:例如Java、C、SQL等。

<body> 
  <script> 
    alert(1);
    alert(2);
    alert(1)
    alert(2)
  </script>
</body>

实际开发中有许多人主张书写 JavaScript 代码时省略结束符 ;

1.1.4 输入和输出

输出和输入也可理解为人和计算机的交互,用户通过键盘、鼠标等向计算机输入信息,计算机处理后再展示结果给用户,这便是一次输入和输出的过程。

举例说明:如按键盘上的方向键,向上/下键可以滚动页面,按向上/下键这个动作叫作输入,页面发生了滚动了这便叫输出。

  • 输出

    JavaScript 可以接收用户的输入,然后再将输入的结果输出:alert()document.wirte()

    以数字为例,向 alert()document.write()输入任意数字,他都会以弹窗形式展示(输出)给用户。

  • 输入

    prompt() 输入任意内容会以弹窗形式出现在浏览器中,一般提示用户输入一些内容。

    <body>
      <script> 
        // 1. 输入的任意数字,都会以弹窗形式展示
        document.write('要输出的内容')
        alert('要输出的内容');
    
        // 2. 以弹窗形式提示用户输入姓名,注意这里的文字使用英文的引号
        prompt('请输入您的姓名:')
      </script>
    </body>
    

1.2 变量

理解变量是计算机存储数据的“容器”,掌握变量的声明方式

变量是计算机中用来存储数据的“容器”,它可以让计算机变得有记忆,通俗的理解变量就是使用【某个符号】来代表【某个具体的数值】(数据)

<script>
  // x 符号代表了 5 这个数值
  x = 5
  // y 符号代表了 6 这个数值
  y = 6
    
  //举例: 在 JavaScript 中使用变量可以将某个数据(数值)记录下来!

  // 将用户输入的内容保存在 num 这个变量(容器)中
  num = prompt('请输入一数字!')

  // 通过 num 变量(容器)将用户输入的内容输出出来
  alert(num)
  document.write(num)
</script>

1.2.1 声明

声明(定义)变量有两部分构成:声明关键字、变量名(标识)

<body>
  <script> 
    // let 变量名
    // 声明(定义)变量有两部分构成:声明关键字、变量名(标识)
    // let 即关键字,所谓关键字是系统提供的专门用来声明(定义)变量的词语
    // age 即变量的名称,也叫标识符
    let age
  </script>
</body>

关键字是 JavaScript 中内置的一些英文词汇(单词或缩写),它们代表某些特定的含义,如 let 的含义是声明变量的,看到 let 后就可想到这行代码的意思是在声明变量,如 let age;

letvar 都是 JavaScript 中的声明变量的关键字,推荐使用 let 声明变量!!!

后面你可能还会看见const来声明变量,不过let声明的变量是可变的,const声明的变量是不可变的。

1.2.2 赋值

声明(定义)变量相当于创造了一个空的“容器”,通过赋值向这个容器中添加数据。

<body>
  <script> 
    // 声明(定义)变量有两部分构成:声明关键字、变量名(标识)
    // let 即关键字,所谓关键字是系统提供的专门用来声明(定义)变量的词语
    // age 即变量的名称,也叫标识符
    let age
    // 赋值,将 18 这个数据存入了 age 这个“容器”中
    age = 18
    // 这样 age 的值就成了 18
    document.write(age)
    
    // 也可以声明和赋值同时进行
    let str = 'hello world!'
    alert(str);
  </script>
</body>

1.2.3 关键字

JavaScript 使用专门的关键字 letvar 来声明(定义)变量,在使用时需要注意一些细节:

以下是使用 let 时的注意事项:

  1. 允许声明和赋值同时进行
  2. 不允许重复声明
  3. 允许同时声明多个变量并赋值
  4. JavaScript 中内置的一些关键字不能被当做变量名

以下是使用 var 时的注意事项:

  1. 允许声明和赋值同时进行
  2. 允许重复声明
  3. 允许同时声明多个变量并赋值

大部分情况使用 letvar 区别不大,但是 let 相较 var 更严谨,因此推荐使用 let,后期会更进一步介绍二者间的区别。

1.2.4 变量名命名规则

关于变量的名称(标识符)有一系列的规则需要遵守:

  1. 只能是字母、数字、下划线、$,且不能能数字开头
  2. 字母区分大小写,如 Age 和 age 是不同的变量
  3. JavaScript 内部已占用于单词(关键字或保留字)不允许使用
  4. 尽量保证变量具有一定的语义,见字知义

注:所谓关键字是指 JavaScript 内部使用的词语,如 letvar,保留字是指 JavaScript 内部目前没有使用的词语,但是将来可能会使用词语。

<body>
  <script> 
    let age = 18 // 正确
    let age1 = 18 // 正确
    let _age = 18 // 正确

    // let 1age = 18; // 错误,不可以数字开头
    let $age = 18 // 正确
    let Age = 24 // 正确,它与小写的 age 是不同的变量
    // let let = 18; // 错误,let 是关键字
    let int = 123 // 不推荐,int 是保留字
  </script>
</body>

1.2.5 常量

概念:使用 const 声明的变量称为“常量”。

使用场景:当某个变量永远不会改变的时候,就可以使用 const 来声明,而不是let。

命名规范:和变量一致

const PI = 3.14

注意: 常量不允许重新赋值,声明的时候必须赋值(初始化)

1.3 数据类型

计算机程序可以处理大量的数据,为了方便数据的管理,将数据分成了不同的类型:

注:通过 typeof 关键字检测数据类型

<body>
  <script> 
    // 检测 1 是什么类型数据,结果为 number
    document.write(typeof 1)
  </script>
</body>

1.3.1 数值类型(number)

即我们数学中学习到的数字,可以是整数、小数、正数、负数

<body>
  <script> 
    let score = 100 // 正整数
    let price = 12.345 // 小数
    let temperature = -40 // 负数

    document.write(typeof score) // 结果为 number
    document.write(typeof price) // 结果为 number
    document.write(typeof temperature) // 结果为 number
  </script>
</body>

JavaScript 中的数值类型与数学中的数字是一样的,分为正数、负数、小数等。

1.3.2 字符串类型(string)

通过单引号( '') 、双引号( "")或反引号(``)包裹的数据都叫字符串,单引号和双引号没有本质上的区别,推荐使用单引号

注意事项:

  1. 无论单引号或是双引号必须成对使用
  2. 单引号/双引号可以互相嵌套,但是不以自已嵌套自已
  3. 必要时可以使用转义符 \,输出单引号或双引号
<body>
  <script> 
    let user_name = '小明' // 使用单引号
    let gender = "男" // 使用双引号
    let str = '123' // 看上去是数字,但是用引号包裹了就成了字符串了
    let str1 = '' // 这种情况叫空字符串
        
    documeent.write(typeof user_name) // 结果为 string
    documeent.write(typeof gender) // 结果为 string
    documeent.write(typeof str) // 结果为 string
  </script>
</body>

1.3.3 布尔类型(boolean)

表示肯定或否定时在计算机中对应的是布尔类型数据,它有两个固定的值 truefalse,表示肯定的数据用 true,表示否定的数据用 false

<body>
  <script> 
    //  pink老师帅不帅?回答 是 或 否
    let isCool = true // 是的,摔死了!
    isCool = false // 不,套马杆的汉子!

    document.write(typeof isCool) // 结果为 boolean
  </script>
</body>

1.3.4 undefined

未定义是比较特殊的类型,只有一个值 undefined,只声明变量,不赋值的情况下,变量的默认值为 undefined,一般很少【直接】为某个变量赋值为 undefined。

<body>
  <script> 
    // 只声明了变量,并末赋值
    let tmp;
    document.write(typeof tmp) // 结果为 undefined
  </script>
</body>

注:JavaScript 中变量的值决定了变量的数据类型。

1.3.5 对象(Object)

对象是 JavaScript 数据类型的一种,之前已经学习了数值类型、字符串类型、布尔类型、undefined。对象数据类型可以被理解成是一种数据集合。它由属性和方法两部分构成

声明一个对象类型的变量与之前声明一个数值或字符串类型的变量没有本质上的区别。

// 声明字符串类型变量
let str = 'hello world!'

// 声明数值类型变量
let num = 199

// 声明对象类型变量,使用一对花括号
// user 便是一个对象了,目前它是一个空对象
let user = {}

数据描述性的信息称为属性,如人的姓名、身高、年龄、性别等,一般是名词性的。

  1. 属性都是成 对出现的,包括属性名和值,它们之间使用英文 : 分隔
  2. 多个属性之间使用英文 , 分隔
  3. 属性就是依附在对象上的变量
  4. 属性名可以使用 ""'',一般情况下省略,除非名称遇到特殊符号如空格、中横线等
// 通过对象描述一个人的数据信息
// person 是一个对象,它包含了一个属性 name
// 属性都是成对出现的,属性名 和 值,它们之间使用英文 : 分隔
let person = {
  name: '小明', // 描述人的姓名
  age: 18, // 描述人的年龄
  stature: 185, // 描述人的身高
  gender: '男', // 描述人的性别
}

声明对象,并添加了若干属性后,可以使用 .[] 获得对象中属性对应的值,我称之为属性访问。

// 通过对象描述一个人的数据信息
// person 是一个对象,它包含了一个属性 name
// 属性都是成对出现的,属性名 和 值,它们之间使用英文 : 分隔
let person = {
  name: '小明', // 描述人的姓名
  age: 18, // 描述人的年龄
  stature: 185, // 描述人的身高
  gender: '男', // 描述人的性别
};

// 访问人的名字
console.log(person.name) // 结果为 小明
// 访问人性别
console.log(person.gender) // 结果为 男
// 访问人的身高
console.log(person['stature']) // 结果为 185
// 或者
console.log(person.stature) // 结果同为 185

扩展:也可以动态为对象添加属性,动态添加与直接定义是一样的,只是语法上更灵活。

// 声明一个空的对象(没有任何属性)
let user = {}
// 动态追加属性
user.name = '小明'
user['age'] = 18

// 动态添加与直接定义是一样的,只是语法上更灵活

数据行为性的信息称为方法,如跑步、唱歌等,一般是动词性的,其本质是函数。

  1. 方法是由方法名和函数两部分构成,它们之间使用 : 分隔
  2. 多个属性之间使用英文 , 分隔
  3. 方法是依附在对象中的函数
  4. 方法名可以使用 ""'',一般情况下省略,除非名称遇到特殊符号如空格、中横线等
// 方法是依附在对象上的函数
let person = {
  name: '小红',
  age: 18,
  // 方法是由方法名和函数两部分构成,它们之间使用 : 分隔
  singing: function () {
    console.log('两只老虎,两只老虎,跑的快,跑的快...')
  },
  run: function () {
    console.log('我跑的非常快...')
  }
}

声明对象,并添加了若干方法后,可以使用 .[] 调用对象中函数,我称之为方法调用。

 // 方法是依附在对象上的函数
let person = {
  name: '小红',
  age: 18,
  // 方法是由方法名和函数两部分构成,它们之间使用 : 分隔
  singing: function () {
    console.log('两只老虎,两只老虎,跑的快,跑的快...')
  },
  run: function () {
    console.log('我跑的非常快...')
  }
}

// 调用对象中 singing 方法
person.singing()
// 调用对象中的 run 方法
person.run()

扩展:也可以动态为对象添加方法,动态添加与直接定义是一样的,只是语法上更灵活。

// 声明一个空的对象(没有任何属性,也没有任何方法)
let user = {}
// 动态追加属性
user.name = '小明'
user.['age'] = 18

// 动态添加方法
user.move = function () {
  console.log('移动一点距离...')
}

注:无论是属性或是方法,同一个对象中出现名称一样的,后面的会覆盖前面的。

null 也是 JavaScript 中数据类型的一种,通常只用它来表示不存在的对象。使用 typeof 检测类型它的类型时,结果为 object

遍历对象:

let obj = {
    uname: 'pink'
}
for(let k in obj) {
    // k是属性名 类型是字符串 带引号:obj.'uname'     k ===  'uname'
    // obj[k]  属性值    obj['uname'] === obj[k]
}

也就是说,使用for…in来遍历的时候,变量名称(k)是属性名称,并且是带有引号的属性名称,所以for…in一般只用来遍历对象

**for in 不提倡遍历数组 因为 k 是 字符串 **

1.4 类型转换

在 JavaScript 中数据被分成了不同的类型,如数值、字符串、布尔值、undefined,在实际编程的过程中,不同数据类型之间存在着转换的关系。

1.4.1 隐式转换

某些运算符被执行时,系统内部自动将数据类型进行转换,这种转换称为隐式转换。

<body>
  <script> 
    let num = 13 // 数值
    let num2 = '2' // 字符串

    // 结果为 132
    // 原因是将数值 num 转换成了字符串,相当于 '13'
    // 然后 + 将两个字符串拼接到了一起
    console.log(num + num2)

    // 结果为 11
    // 原因是将字符串 num2 转换成了数值,相当于 2
    // 然后数值 13 减去 数值 2
    console.log(num - num2)

    let a = prompt('请输入一个数字')
    let b = prompt('请再输入一个数字')

    alert(a + b);
  </script>
</body>

有如下代码:

console.log(1+1) //2 (number)
console.log('hh'+1) //hh1 (string)
console.log(2+2) //4 (number)
console.log(2+'2') //22 (string)
console.log(2-2) //0 (number)
console.log(2-'2') //0 (number)
console.log(+12) //12 (number)
console.log(+'123') //123 (number)

总结:

前提是字符串也是数字形式,例如‘123’;不能够是‘hh123’;

  • string类型与number类型做+,结果为string
  • string类型与number类型做-,结果为number
  • string类型前只有+时,结果为number
  • number类型前只有+时,结果为number

1.4.2 显式转换

编写程序时过度依靠系统内部的隐式转换是不严禁的,因为隐式转换规律并不清晰,大多是靠经验总结的规律。为了避免因隐式转换带来的问题,通常根逻辑需要对数据进行显示转换。

Number

通过 Number 显示转换成数值类型,当转换失败时结果为 NaN(Not a Number)即不是一个数字。

<body>
  <script>
    let t = '12'
    let f = 8

    // 显式将字符串 12 转换成数值 12
    t = Number(t)

    // 检测转换后的类型
    // console.log(typeof t);
    console.log(t + f) // 结果为 20

    // 并不是所有的值都可以被转成数值类型
    let str = 'hello'
    // 将 hello 转成数值是不现实的,当无法转换成
    // 数值时,得到的结果为 NaN (Not a Number)
    console.log(Number(str))
  </script>
</body>

1.5 运算符

这里的运算符的用法和其他编程语言的运算符用法一致,唯一不同的是:===运算符;表示左右两边是否类型都相等,而==只判断值是否相等。

1.6 语句

这里的语句有循环、判断、分支语句,使用方法和Java类似不多说

1.7 数组

数组:(Array)是一种可以按顺序保存数据的数据类型

使用场景:如果有多个数据可以用数组保存起来,然后放到一个变量中,管理非常方便

例如:

<script>
  // 1. 语法,使用 [] 来定义一个空数组
  // 定义一个空数组,然后赋值给变量 classes
  // let classes = [];

  // 2. 定义非空数组
  let classes = ['小明', '小刚', '小红', '小丽', '小米']
</script>

通过 [] 定义数组,数据中可以存放真正的数据,如小明、小刚、小红等这些都是数组中的数据,我们这些数据称为数组单元,数组单元之间使用英文逗号分隔。

数组下标的概念和Java语言中类似,不多说。

1.7.1 数据单元值类型

数组做为数据的集合,它的单元值可以是任意数据类型

例如:

<script>
  // a) 数组单元值的类型为字符类型
  let list = ['HTML', 'CSS', 'JavaScript']
  // b) 数组单元值的类型为数值类型
  let scores = [78, 84, 70, 62, 75]
  // c) 混合多种类型
  let mixin = [true, 1, false, 'hello']
</script>

数组在 JavaScript 中并不是新的数据类型,它属于对象类型(Object)。

1.7.2 操作数组

数组做为对象数据类型,不但有 length 属性可以使用,还提供了许多方法:

  1. push 动态向数组的尾部添加一个单元
  2. unshit 动态向数组头部添加一个单元
  3. pop 删除最后一个单元
  4. shift 删除第一个单元
  5. splice 动态删除/添加任意单元:splice(起始位置, 删除的个数)splice(起始位置,删除个数,添加数组元素)

使用以上5个方法时,都是直接在原数组上进行操作,即成功调任何一个方法,原数组都跟着发生相应的改变。并且在添加或删除单元时 length 并不会发生错乱。

<script>
    // 定义一个数组
    let arr = ['html', 'css', 'javascript']

    // 1. push 动态向数组的尾部添加一个单元
    arr.push('Nodejs')
    console.log(arr) //['html', 'css', 'javascript','Nodejs']
    arr.push('Vue')

    // 2. unshit 动态向数组头部添加一个单元
    arr.unshift('VS Code') //['VS Code','html', 'css', 'javascript']
    console.log(arr)

    // 3. splice 动态删除任意单元
    arr.splice(2, 1) // 从索引值为2的位置开始删除1个单元
    console.log(arr) //['VS Code', 'html', 'javascript', 'Nodejs', 'Vue']

    // 4. pop 删除最后一个单元
    arr.pop()
    console.log(arr) //['VS Code', 'html', 'javascript', 'Nodejs']

    // 5. shift 删除第一个单元
    arr.shift()
    console.log(arr) //['html', 'javascript', 'Nodejs']
</script>

可见都是在原数组上直接操作的。

还有两个拼接字符串的方法:

  • map:遍历数组处理数据,并且返回新的数组

    如果你有Java基础的,可以理解为Java中stream流中的map

    const arr = ['red','blue','yellow'];
    const newArr =  arr.map(function (element,index) {
        console.log(element); //第一个参数表示数组中的每个元素
        console.log(index); //第二个参数表示数组的下标
        return element+index; //使用map方法最好返回一个结果,让他生成一个新数组,这是设计map的初衷
    });
    console.log(newArr);
    

    执行结果

  • join:把数组中的所有元素转换一个字符串

    const arr = ['red','blue','yellow'];
    console.log(arr.join()) //默认是使用逗号来分割
    console.log(arr.join('')) //空字符串表示不分割
    console.log(arr.join('|')) //join参数中写啥表示用啥分割字符串
    

    执行结果

1.7.3 forEach

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数

语法:

被遍历的数组.forEach(function(当前数组元素,当前元素索引号){
    // 函数体,无返回值
})

与map最大的区别就是forEach没有返回值,而map有返回值。

注意:

  1. forEach主要用于遍历数组
  2. 参数当前元素是必须要写的,索引号可选
  3. 适用于遍历数组对象

1.7.4 filter

filter()方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素

主要使用场景:筛选数组符合条件的元素,并返回筛选之后元素的新数组。

语法:

被遍历的数组.filter(function(currentValue,index){
    retrun 筛选条件
})

注意:

  • filter()筛选数组
  • 返回值:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组
  • 参数:currentValue必须写,index可选
  • 因为返回新数组,所以不会影响原数组

1.8 函数

1.8.1 声明

函数结构形式

1.8.2 调用

声明(定义)的函数必须调用才会真正被执行,使用 () 调用函数。

// 声明(定义)了最简单的函数,既没有形式参数,也没有返回值
function sayHi() {
  console.log('嗨~')
}
// 函数调用,这些函数体内的代码逻辑会被执行
// 函数名()

sayHi()
// 可以重复被调用,多少次都可以
sayHi()

注:函数名的命名规则与变量是一致的,并且尽量保证函数名的语义。

1.8.3 参数

参数以及传参的形式都和Java类似不多说。

1.8.4 返回值

和Java类似,使用return返回即可。不同的地方在于:函数如果没有return,这种情况默认返回值为 undefined

1.8.5 匿名函数

函数的分类共有3中划分:

  1. 具名函数:也就是有名字的函数

    function fn() {} //声明
    fn() //调用
    
  2. 匿名函数:也就是没有名字的函数

    function(){}//声明
    

    匿名函数因为没有名字,所以不能够直接使用。

    一般我们使用匿名函数都是当作回调函数来使用的,例如:

    setInterval(function () {
        //业务逻辑....
    },1000)
    

    这里的处理业务逻辑的函数就是匿名函数,用在了setInterval函数中,所以它也是一个回调函数。

  3. 立即执行函数:没有名字的函数,并且会立即执行

    (function () {
        //业务处理....
    })()
    

    立即执行函数无需调用就会立即执行,因为第一个()里面是匿名函数,然后后面又有一个(),你可以把第二个小括号理解为调用匿名函数。

    所以一般写立即执行函数的时候,我们先写第一个小括号,之后在里面写一个匿名函数,最后在末尾在添加一个小括号表示调用。

1.9 内置对象

回想一下我们曾经使用过的 console.logconsole其实就是 JavaScript 中内置的对象,该对象中存在一个方法叫 log,然后调用 log 这个方法,即 console.log()

除了 console 对象外,JavaScript还有其它的内置的对象

1.9.1 Math

Math 是 JavaScript 中内置的对象,称为数学对象,这个对象下即包含了属性,也包含了许多的方法。

  • 属性

    // 圆周率
    console.log(Math.PI);
    
  • 方法

    Math.random,生成 0 到 1 间的随机数

    // 0 ~ 1 之间的随机数, 包含 0 不包含 1
    Math.random()
    

    Math.ceil,数字向上取整

    // 舍弃小数部分,整数部分加1
    Math.ceil(3.4) //4
    

    Math.floor,数字向下取整

    // 舍弃小数部分,整数部分不变
    Math.floor(4.68) //4
    

    Math.round,四舍五入取整

    // 取整,四舍五入原则
    Math.round(5.46539)
    Math.round(4.849)
    

    Math.max,在一组数中找出最大的

    // 找出最大值
    Math.max(10, 21, 7, 24, 13)
    

    Math.min,在一组数中找出最小的

    // 找出最小值
    Math.min(24, 18, 6, 19, 21)
    

    Math.pow,幂方法

    // 求某个数的多少次方
    Math.pow(4, 2) // 求 4 的 2 次方
    Math.pow(2, 3) // 求 2 的 3 次方
    

    Math.sqrt,平方根

    // 求某数的平方根
    Math.sqrt(16)
    

    数学对象提供了比较多的方法,这里不要求强记,通过演示数学对象的使用,加深对对象的理解。

2. APIS

2.1 介绍

严格意义上讲,我们在 JavaScript 阶段学习的知识绝大部分属于 ECMAScript 的知识体系,ECMAScript 简称 ES 它提供了一套语言标准规范,如变量、数据类型、表达式、语句、函数等语法规则都是由 ECMAScript 规定的。浏览器将 ECMAScript 大部分的规范加以实现,并且在此基础上又扩展一些实用的功能,这些被扩展出来的内容我们称为 Web APIs

ECMAScript 运行在浏览器中然后再结合 Web APIs 才是真正的 JavaScript,Web APIs 的核心是 DOM 和 BOM。

扩展阅读:ECMAScript 规范在不断的更新中,存在多个不同的版本,早期的版本号采用数字顺序编号如 ECMAScript3、ECMAScript5,后来由于更新速度较快便采用年份做为版本号,如 ECMAScript2017、ECMAScript2018 这种格式,ECMAScript6 是 2015 年发布的,常叫做 EMCAScript2015。

关于 JavaScript 历史的扩展阅读

DOM(Document Object Model)是将整个 HTML 文档的每一个标签元素视为一个对象,这个对象下包含了许多的属性和方法,通过操作这些属性或者调用这些方法实现对 HTML 的动态更新,为实现网页特效以及用户交互提供技术支撑。

简言之 DOM 是用来动态修改 HTML 的,其目的是开发网页特效及用户交互。

观察一个小例子:

上述的例子中当用户分分别点击【开始】或【结束】按钮后,通过右侧调试窗口可以观察到 html 标签的内容在不断的发生改变,这便是通过 DOM 实现的。

2.1.2 概念

  • DOM 树:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>标题</title>
    </head>
    <body>
      文本
      <a href="">链接名</a>
      <div id="" class="">文本</div>
    </body>
    </html>
    

    如下图所示,将 HTML 文档以树状结构直观的表现出来,我们称之为文档树或 DOM 树,文档树直观的体现了标签与标签之间的关系。

    DOM

  • DOM 节点:

    节点是文档树的组成部分,每一个节点都是一个 DOM 对象,主要分为元素节点、属性节点、文本节点等。

    1. 【元素节点】其实就是 HTML 标签,如上图中 headdivbody 等都属于元素节点。
    2. 【属性节点】是指 HTML 标签中的属性,如上图中 a 标签的 href 属性、div 标签的 class 属性。
    3. 【文本节点】是指 HTML 标签的文字内容,如 title 标签中的文字。
    4. 【根节点】特指 html 标签。
    5. 其它…
  • document:

    document 是 JavaScript 内置的专门用于 DOM 的对象,该对象包含了若干的属性和方法,document 是学习 DOM 的核心。

    <script>
      // document 是内置的对象
      // console.log(typeof document);
    
      // 1. 通过 document 获取根节点
      console.log(document.documentElement); // 对应 html 标签
    
      // 2. 通过 document 节取 body 节点
      console.log(document.body); // 对应 body 标签
    
      // 3. 通过 document.write 方法向网页输出内容
      document.write('Hello World!');
    </script>
    

    上述列举了 document 对象的部分属性和方法,我们先对 document 有一个整体的认识。

2.2 获取DOM对象

  1. querySelector:满足条件的第一个元素
  2. querySelectorAll:满足条件的元素集合,返回伪数组
  3. 了解其他方式:getElementByIdgetElementsByTagName这两种方式现在基本淘汰了

注意:

querySelectorAll返回的是一个伪数组,为什么是伪数组呢?

  1. 具有length属性
  2. 按索引方式存储数据
  3. 不具有数组的方法

那为什么现在getElementByIdgetElementsByTagName又基本上不用了呢,因为querySelectorquerySelectorAll中里面可以直接写选择器:

例如:

<div>你好</div>
<div class="hello">hello</div>
<div id="hi">hi</div>
<script>
    let div1 = document.querySelector('div');
    let div2 = document.querySelector('.hello');
    let div3 = document.querySelector('#hi');
    console.log(div1.innerHTML)
    console.log(div2.innerHTML)
    console.log(div3.innerHTML)
</script>

结果

可见都获取成功了,所以我们一般使用querySelectorquerySelectorAll,里面写选择器即可,CSS中选择器怎么写,这里就怎么写

2.3 操作元素内容

通过修改 DOM 的文本内容,动态改变网页的内容。

  1. innerText 将文本内容添加/更新到任意标签位置,文本中包含的标签不会被解析。

    <body>
    <div class="d1">猜猜我是谁</div>
    <div class="d2">猜猜他是谁</div>
    <script>
        const div1 = document.querySelector('.d1')
        div1.innerText = '念心卓'
        const div2 = document.querySelector('.d2')
        div2.innerText = '<h1>不知道</h1>'
    </script>
    </body>
    

    可见h1标签并没有被解析

  2. innerHTML 将文本内容添加/更新到任意标签位置,文本中包含的标签会被解析。

    <body>
    <div class="d1">猜猜我是谁</div>
    <div class="d2">猜猜他是谁</div>
    <script>
        const div1 = document.querySelector('.d1')
        div1.innerText = '念心卓'
        const div2 = document.querySelector('.d2')
        div2.innerHTML = '<h1>不知道</h1>'
    </script>
    </body>
    

    h1标签已被解析

总结:如果文本内容中包含 html 标签时推荐使用 innerHTML,否则建议使用 innerText 属性。

我们一般直接使用innerHTML即可

2.3.1 常用属性修改

直接通过访问属性的方式来修改即可:

// 1. 获取 img 对应的 DOM 元素
const pic = document.querySelector('.pic')
// 2. 修改属性
pic.src = './images/lion.webp'
pic.width = 400;
pic.alt = '图片不见了...'

2.3.2 控制样式属性

  1. 应用【修改样式】,通过修改行内样式 style 属性,实现对样式的动态修改。**(不推荐)**

    通过元素节点获得的 style 属性本身的数据类型也是对象,如 box.style.colorbox.style.width 分别用来获取元素节点 CSS 样式的 colorwidth 的值。

    <body>
      <div class="box">随便一些文本内容</div>
      <script>
        // 获取 DOM 节点
        const box = document.querySelector('.box')
        box.style.color = 'red'
        box.style.width = '300px'
        // css 属性的 - 连接符与 JavaScript 的 减运算符冲突,所以要改成小驼峰法
        box.style.backgroundColor = 'pink'
      </script>
    </body>
    

    样式修改成功

    任何标签都有 style 属性,通过 style 属性可以动态更改网页标签的样式,如要遇到 css 属性中包含字符 - 时,要将 - 去掉并将其后面的字母改成大写,如 background-color 要写成 box.style.backgroundColor

  2. 操作类名(className) 操作CSS(推荐)

    如果修改的样式比较多,直接通过style属性修改比较繁琐,我们可以通过借助于css类名的形式。

    <style>
        .a {
            color: rebeccapurple;
        }
        .box {
            width: 200px;
            height: 200px;
            background: pink;
        }
    
    </style>
    <body>
    <div class="a">随便一些文本内容</div>
    <script>
        // 获取 DOM 节点
        const box = document.querySelector('.a')
        box.className = 'box' 
    </script>
    </body>
    

    可见操作成功

    注意:

    1. 由于class是关键字, 所以使用className属性去代替
    2. className是使用新值换旧值, 如果需要添加一个类,需要保留之前的类名;例如原来的类名为:a,那么你想保留原来的类名,你现在的写法就为:box.className = 'a box'
    3. 添加类名使用className属性,并且无需写.,直接写类名即可
  3. 通过 classList 操作类控制CSS(推荐)

    为了解决className 容易覆盖以前的类名,我们可以通过classList方式追加和删除类名

    <style>
        .a {
            color: rebeccapurple;
        }
        .box {
            width: 200px;
            height: 200px;
            background: pink;
        }
    
        .b {
            color: red;
        }
    </style>
    <body>
    <div class="a">随便一些文本内容</div>
    <script>
        // 获取 DOM 节点
        const box = document.querySelector('.a')
        //追加box类名
        //box.classList.add('box')
    
        //删除a 类名
        //box.classList.remove('a')
    
        //切换类名(有贼删除,无则添加)
        box.classList.toggle('b')
    </script>
    </body>
    

    自己去试验效果即可。

    拓展:元素.classList.contains() 看看有没有包含某个类,如果有则返回true,么有则返回false

2.3.3 操作表单元素属性

表单很多情况,也需要修改属性,比如点击眼睛,可以看到密码,本质是把表单类型(type)转换为文本框(text);或者说表单的复选框,通过调整checked属性来表示是否选中;获取表单的内容值一般使用xxx.value来获取输入的值,而不能使用xxx.innerHTML;

例如:

<body>
<input type="text" class="i1"> <br>
<input type="checkbox" class="i2" > 复选框;默认是没有选中的 <br>
<button >操作按钮</button>
<script>
    const input = document.querySelector('.i1')
    //监听输入事件
    input.addEventListener('input',function (){
        //获取到输入的值
        console.log(input.value);
    })

    const checkbox = document.querySelector('.i2')
    //修改选择
    checkbox.checked = true //这里最好写:true,不要写:'true'

    const button = document.querySelector('button')
    button.disabled = true //表示按钮无法操作
</script>
</body>

结果

案例:实现全选

<body>
<input type="checkbox" class="checkedAll"> 全选 <br>
<input type="checkbox" class="checkChild"> 选择1 <br>
<input type="checkbox" class="checkChild"> 选择2 <br>
<input type="checkbox" class="checkChild"> 选择3 <br>
<input type="checkbox" class="checkChild"> 选择4 <br>
<input type="checkbox" class="checkChild"> 选择5 <br>
<script>
    //首先获取所有的子选项
    const checkChilds = document.querySelectorAll('.checkChild');
    //其次获取全选项
    const checkAll = document.querySelector('.checkedAll');
    //每次点击子选项来更新选中状态,如果子选项全部选了,那么全选也要被选
    let count = 0;
    for (let i = 0; i < checkChilds.length; i++) {
        checkChilds[i].addEventListener('click',function (){
            if (this.checked){
                count++;
            }else {
                count--;
            }
            //如果所有的子选项被选中了,那么就选中全选选项
            checkAll.checked = count === checkChilds.length;
        });
    }
    //全选实现
    checkAll.addEventListener('click',function (){
        for (let i = 0; i < checkChilds.length; i++) {
            checkChilds[i].checked = checkAll.checked;
        }
    })
</script>
</body>

代码的验证可以自己去验证。

2.3.4 自定义属性

标准属性: 标签天生自带的属性 比如class id title等, 可以直接使用点语法操作比如: disabled、checked、selected

自定义属性:在html5中推出来了专门的data-自定义属性 ,在标签上一律以data-开头,在DOM对象上一律以dataset对象方式获取

例如:

<body>
   <div data-id="1"> 自定义属性 </div>
    <script>
        // 1. 获取元素
        let div = document.querySelector('div')
        // 2. 获取自定义属性值
         console.log(div.dataset.id)
      
    </script>
</body>

上述代码的自定义属性为:data-id,其实表达的意思就是自定义了一个id属性,不过data-开头是要求的标准形式,然后获取自定义的属性:xxx.dataset.自定义

2.4 间歇函数(setInterval)

利用间歇函数创建定时任务。

setInterval 是 JavaScript 中内置的函数,它的作用是间隔固定的时间自动重复执行另一个函数,也叫定时器函数

<script>
  // 1. 定义一个普通函数
  function repeat() {
    console.log('不知疲倦的执行下去....')
  }

  // 2. 使用 setInterval 调用 repeat 函数
  // 间隔 1000 毫秒,重复调用 repeat
  setInterval(repeat, 1000)
</script>

注意:在setInterval调用其他函数的时候,只能写函数名;例如上诉代码,只能在setInterval中写repeat,不能写repeat(),因为如果你写repeat()就表示,每隔一秒中执行repeat函数返回的结果,而不是执行函数。

我们也常常使用匿名函数的写法来完成:

setInterval(function () {
   console.log('每隔一秒中执行一次')
},1000)

2.5 事件

事件是编程语言中的术语,它是用来描述程序的行为或状态的,一旦行为或状态发生改变,便立即调用一个函数。

例如:用户使用【鼠标点击】网页中的一个按钮、用户使用【鼠标拖拽】网页中的一张图片

2.5.1 事件监听

结合 DOM 使用事件时,需要为 DOM 对象添加事件监听,等待事件发生(触发)时,便立即调用一个函数。

addEventListener 是 DOM 对象专门用来添加事件监听的方法,它的两个参数分别为【事件类型】和【事件回调】,事件回调也就是发生了这个事件要处理的事情。

某个原始.addEventListener('事件类型',回调函数)

例如:

<input type="text">
<script>
    const input = document.querySelector('input');
    input.addEventListener('input',function (){
        console.log(input.value);
    })
</script>
</body>

上诉代码中,我给input输入框绑定了一个事件(addEventListener),绑定的事件类型是输入(input),回调的函数是打印我输入的值。

可见我每次输入一个值就会打印出来当前的输入值

对于事件监听函数里面的事件回调函数来说,这个函数会在事件被触发时立即被调用,在这个函数中可以编写任意逻辑的代码,如改变 DOM 文本颜色、文本内容等。

2.5.2 事件类型

  • 鼠标事件

    mouseenter :监听鼠标是否移入 DOM 元素

    mouseleave :监听鼠标是否移出 DOM 元素

    click:鼠标单机事件

    dblclick:鼠标双击事件

  • 键盘事件

    keydown:键盘按下触发
    keyup:键盘抬起触发

  • 焦点事件

    focus:获得焦点时触发

    blur:失去焦点时触发

    一般针对输入框

  • 文本框输入事件

    input:输入时触发

    补充:给input注册 change 事件,值被修改并且失去焦点后触发

2.5.3 事件对象

任意事件类型被触发时与事件相关的信息会被以对象的形式记录下来,我们称这个对象为事件对象。

某个原始.addEventListener('事件类型',function(e){}) //这个e就是事件对象,可以叫event、ev、e等

例如:

<body>
<input type="text">
<script>
    const input = document.querySelector('input');
    input.addEventListener('input',function (e){
        console.log(e);
    })
</script>
</body>

可见事件对象里面包含了很多信息

事件回调函数的【第1个参数】即所谓的事件对象,通常习惯性的将这个对数命名为 eventevev

接下来简单看一下事件对象中包含了哪些有用的信息:

  1. ev.type :当前事件的类型
  2. ev.clientX/Y: 光标相对浏览器窗口的位置
  3. ev.offsetX/Y :光标相于当前 DOM 元素的位置

注:在事件回调函数内部通过 window.event 同样可以获取事件对象。

2.5.4 事件解绑

之前给某个标签绑定了事件之后,我们现在需要给事件进行解绑操作:

xxx.removeEventListener('事件类型',函数名称)

例如:

const li = document.querySelector('li');
function getListener() {
    console.log(111)
}
li.addEventListener('click',getListener);
li.removeEventListener('click',getListener) //事件解绑

从代码中可以看出,我们如果将来要解绑事件的话,在绑定阶段就不能够写匿名函数,而是必须要提出来单独写函数,这样在解绑的时候才能够写函数名来解绑。

2.5.5 阻止事件默认行为

我们某些情况下需要阻止默认行为的发生,比如阻止链接的跳转,表单域的跳转

语法:事件对象.preventDefault()

例如我现在有如下代码:

<form action="http://www.baidu.com" >
    <input type="submit" value="提交">
</form>

我的要求是点击提交之后阻止他跳转到百度,实现代码如下:

const submit = document.querySelector('input');
submit.addEventListener('click',function (ev) {
    ev.preventDefault();
});

测试之后发现,点击提交之后,并不会跳转到百度

2.6 环境对象(this)

环境对象指的是函数内部特殊的变量 this ,它代表着当前函数运行时所处的环境。

  • this 本质上是一个变量,数据类型为对象

  • 函数的调用方式不同 this 变量的值也不同

  • 【谁调用 this 就是谁】是判断 this 值的粗略规则

    例如有如下代码:

    for (let i = 0; i < checkChilds.length; i++) {
        checkChilds[i].addEventListener('click',function (){
            if (this.checked){
                count++;
            }else {
                count--;
            }
            //如果所有的子选项被选中了,那么就选中全选选项
            checkAll.checked = count === checkChilds.length;
        });
    }
    

    因为是checkChilds[i]调用了函数,所以函数里面的this就是当前的checkChilds[i]

  • 如果this直接使用在了外部,没有在某个具体的函数中,那么this就等于window

    <script>
        console.log(this)
        console.log(window)
    </script>
    

    可见是一样的

2.7 回调函数

如果将函数 A 做为参数传递给函数 B 时,我们称函数 A 为回调函数

例如:

<script>
  // 声明 foo 函数
  function foo(arg) {
    console.log(arg);
  }

  // 普通的值做为参数
  foo(10);
  foo('hello world!');
  foo(['html', 'css', 'javascript']);

  function bar() {
    console.log('函数也能当参数...');
  }
  // 函数也可以做为参数!!!!
  foo(bar);
</script>

函数 bar 做参数传给了 foo 函数,bar 就是所谓的回调函数了!!!

我们回顾一下间歇函数 setInterval

<script>
    function fn() {
    console.log('我是回调函数...');
  }
  // 调用定时器
  setInterval(fn, 1000);
</script>

fn 函数做为参数传给了 setInterval ,这便是回调函数的实际应用了,还有另一种更常见写法。

<script>
  // 调用定时器,匿名函数做为参数
  setInterval(function () {
    console.log('我是回调函数...');
  }, 1000);
</script>

结论:

  1. 回调函数本质还是函数,只不过把它当成参数使用
  2. 使用匿名函数做为回调函数比较常见

2.8 事件流

事件流是对事件执行过程的描述,了解事件的执行过程有助于加深对事件的理解,提升开发实践中对事件运用的灵活度。

event

如上图所示,任意事件被触发时总会经历两个阶段:【捕获阶段】和【冒泡阶段】。简言之,捕获阶段是【从父到子】的传导过程,冒泡阶段是【从子向父】的传导过程

2.8.1 捕获和冒泡

了解了什么是事件流之后,我们来看事件流是如何影响事件执行的:

冒泡模式:

<style>
    .father {
        width: 400px;
        height: 400px;
        background-color: deeppink;
    }
    .son {
        width: 200px;
        height: 200px;
        background-color: pink;
    }
</style>
<body>
<div class="father">
    <div class="son"></div>
</div>
<script>
    const father = document.querySelector('.father');
    const son = document.querySelector('.son');
    father.addEventListener('click',function (){
        console.log('father....');
    });
    son.addEventListener('click',function () {
        console.log('son....');
    });
</script>

执行结果

执行上述代码后发现,当我单击father子模块son的时候,father模块的单击事件也被执行了,这是为什么呢?

结合事件流的特征,我们知道当某个元素的事件被触发时,事件总是会先经过其祖先才能到达当前元素,然后再由当前元素向祖先传递,事件在流动的过程中遇到相同的事件便会被触发

再来关注一个细节就是事件相继触发的【执行顺序】,事件的执行顺序是可控制的,即可以在捕获阶段被执行,也可以在冒泡阶段被执行。

如果事件是在冒泡阶段执行的,我们称为冒泡模式,它会先执行子盒子事件再去执行父盒子事件默认是冒泡模式

如果事件是在捕获阶段执行的,我们称为捕获模式,它会先执行父盒子事件再去执行子盒子事件

两种模式的前提是事件的类型都必须保持一致。

捕获模式:

增加addEventListener的第三个参数:

<div class="father">
    <div class="son"></div>
</div>
<script>
    const father = document.querySelector('.father');
    const son = document.querySelector('.son');
    father.addEventListener('click',function (){
        console.log('father....');
    },true);
    son.addEventListener('click',function () {
        console.log('son....');
    },true);
</script>

执行结果

执行的结果可见,捕获模式是由外到内,所以,虽然我是点击的son模块,但是确实由父模块触发然后到子模块的。

结论:

  1. addEventListener 第3个参数决定了事件是在捕获阶段触发还是在冒泡阶段触发
  2. addEventListener 第3个参数为 true 表示捕获阶段触发,false 表示冒泡阶段触发,默认值为 false
  3. 事件流只会在父子元素具有相同事件类型时才会产生影响
  4. 绝大部分场景都采用默认的冒泡模式(其中一个原因是早期 IE 不支持捕获)

2.8.2 阻止冒泡

任何事物由好处也有坏处,冒泡也是。

阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。

调用事件对象的stopPropagation方法即可:

xxx.addEventListener('事件类型',function(e){
    e.stopPropagation()//阻止冒泡
})

代码修改:

const father = document.querySelector('.father');
const son = document.querySelector('.son');
father.addEventListener('click',function (e){
    console.log('father....');
    e.stopImmediatePropagation(); //阻止冒泡(父模块可以不用加)
});
son.addEventListener('click',function (e) {
    console.log('son....');
    e.stopImmediatePropagation(); //阻止冒泡
});

执行结果

鼠标经过事件:

  • mouseover mouseout 有冒泡效果

  • mouseenter mouseleave 没有冒泡效果 (推荐)

2.9 事件委托

事件委托是利用事件流的特征解决一些现实开发需求的知识技巧,主要的作用是提升程序效率。

大量的事件监听是比较耗费性能的,现在我的需求如下:我有很多li,每当我点击某个li的时候,他就变红。

如下代码所示:

<ul>
    <li>第1个小li</li>
    <li>第2个小li</li>
    <li>第3个小li</li>
    <li>第4个小li</li>
    <li>第5个小li</li>
</ul>
<script>
    const lis = document.querySelectorAll('li');
    for (let i = 0; i < lis.length; i++) {
        lis[i].addEventListener('click',function () { //为每一个li都绑定监听事件
            this.style.color = 'red';
        })
    }
</script>

利用事件流的特征,可以对上述的代码进行优化,事件的的冒泡模式总是会将事件流向其父元素的,如果父元素监听了相同的事件类型,那么父元素的事件就会被触发并执行,正是利用这一特征对上述代码进行优化,如下代码所示:

<ul>
    <li>第1个小li</li>
    <li>第2个小li</li>
    <li>第3个小li</li>
    <li>第4个小li</li>
    <li>第5个小li</li>
</ul>
<script>
    const ul = document.querySelector('ul');
    ul.addEventListener('click',function (ev) {
        console.log(ev) //查看一下ev
    })
</script>
</body>

我们的最终目的是保证只有点击 ul子元素才去执行事件的回调函数,如何判断用户点击是哪一个子元素呢

事件对象中的属性 targetsrcElement属性表示真正触发事件的元素,它是一个元素类型的节点

<body>
<ul>
    <li>第1个小li</li>
    <li>第2个小li</li>
    <li>第3个小li</li>
    <li>第4个小li</li>
    <li>第5个小li</li>
</ul>
<script>
    const ul = document.querySelector('ul');
    ul.addEventListener('click',function (ev) {
        console.log(ev)
        if (ev.target.tagName === 'LI'){ //只有点击的标签为li才会执行
            ev.target.style.color = 'red';
        }
    })
</script>
</body>

优化过的代码只对祖先元素添加事件监听,相比对 10000 个元素添加事件监听执行效率要高许多!!!

2.10 其他事件

2.10.1 页面加载事件

在以前,我们喜欢将js代码写在body上方,但是我们知道,代码的执行是从上到下执行的,如果还没有执行到body的时候,先执行js中的代码可能就会出错:

<script>
    const button = document.querySelector('button');
    button.addEventListener('click',function () {
        console.log(123)
    })
</script>
<body>
<button>提交</button>
</body>

结果

出现这个原因就是因为还没有执行到body中的代码,就先执行了js的代码,所以导致了button为null,所以要解决这个问题,就要使用页面加载事件。

加载外部资源(如图片、外联CSS和JavaScript等)加载完毕时触发的事件;有些时候需要等页面资源全部处理完了做一些事情

事件名:load

监听页面所有资源加载完毕:

window.addEventListener('load', function() {
    // 处理的事情
})

代码修改:

<script>
    window.addEventListener('load', function() { //给window(顶级容器)绑定页面加载事件
        const button = document.querySelector('button');
        button.addEventListener('click',function () {
            console.log(123)
        })
    })
</script>
<body>
<button>提交</button>
</body>

成功

2.10.2 DOM加载事件

当 HTML 文档完全解析,且所有延迟脚本下载和执行完毕后,会触发 DOMContentLoaded 事件。它不会等待图片、子框架和异步脚本等其他内容完成加载

DOMContentLoaded与load的区别:

DOMContentLoadedload 是两个与页面加载相关的事件,它们在 JavaScript 中用于执行代码或处理操作,但触发的时机有所不同。

  1. DOMContentLoaded 事件:

    • DOMContentLoaded 事件在 HTML 文档被完全加载和解析之后触发,无需等待样式表、图片和子框架等资源的加载完成。也就是说,当 DOM 树构建完成后,就会触发 DOMContentLoaded 事件。这是一个早于 load 事件的触发时机。
    • 此事件非常适合在页面的 DOM 结构已经准备好,但其他资源(如图片、样式表等)可能尚未加载完成时执行脚本。
    document.addEventListener('DOMContentLoaded', function () {
        // 在DOM准备就绪时执行的代码
    });
    
  2. load 事件:

    • load 事件在整个页面及其所有依赖资源(包括样式表、图片、嵌套的框架等)完全加载完成后触发。这意味着在触发 load 事件时,页面上的所有资源都已经加载完毕。
    • load 事件通常用于执行那些需要等待所有资源加载完成后才能安全执行的代码。
    window.addEventListener('load', function () {
        // 在页面及其所有资源加载完成后执行的代码
    });
    

DOMContentLoaded 相对于load的速度较快。

<script>
    document.addEventListener('DOMContentLoaded',function () {
        const button = document.querySelector('button');
        button.addEventListener('click',function () {
            console.log(123)
        })
    })
</script>
<body>
<button>提交</button>
</body>

一样能够达到效果。

2.10.3 元素滚动事件

滚动条在滚动的时候持续触发的事件;事件类型:scroll

语法:

window.addEventListener('scroll', function() {
    // xxxxx
})

示例代码:

<style>
    body {
        height: 3000px;
    }
</style>
<body>
<script>
    window.addEventListener('scroll',function () {
        console.log('我滚了')
    })
</script>
</body>

执行结果:只要我一拖动滚动条,就会执行滚动事件。

但是这其实并没有实际的意义,我们一般是要求拖动多少距离才执行。

所有这里就涉及到位置的获取:

位置获取,并且是可读可写

例如:

<style>
    body {
        height: 3000px;
    }
</style>
<body>
<script>
    window.addEventListener('scroll',function () {
        //首先获取到html
        const html = document.documentElement;
        console.log(`被卷去了${html.scrollTop}px`);
    })
</script>
</body>

滑动滚动条的结果

注意这里是必须获取html,不能使用window。

2.10.4 页面尺寸事件

响应式中用的很多

会在窗口尺寸改变的时候触发事件,事件类型:resize

语法:

window.addEventListener('resize', function() {
    // xxxxx
})

现在,只要我的浏览器在变大变小,这个事件就会触发。

获取元素的宽高:clientWidthclientHight,这两个都是将padding包括进去了,但是不包括border。

例如:

<body>
<div>1234567788</div>
</body>

我现在要获取这div中内容的宽度,在以前是无法获取的,但是现在,你可以通过clientWidth来获取:

<style>
    div {
        display: inline-block;
    }
</style>
<body>
<div>1234567788</div>
<script>
    const div = document.querySelector('div');
    console.log(div.clientWidth);
</script>
</body>

可见使用了94像素

2.10.5 元素尺寸与位置

简单来说就是通过js的方式,来得到元素在页面中的位置

与client有点小区别

总结:

总结

2.11 日期对象

ECMAScript 中内置了获取系统时间的对象 Date,使用 Date 时与之前学习的内置对象 console 和 Math 不同,它需要借助 new 关键字才能使用。

凡是通过new关键字造出来的对象都称为对象实例化。

基本使用:

//实例化日期对象
const date = new Date();
console.log(date);
//指定事件
const date1 = new Date('2023-12-3');
console.log(date1)

2.11.1 日期对象的方法

方法 作用 说明
getFullYear() 获取年份 获取四位年份
getMonth() 获取月份 取值为0~11
getDate() 获取月份中的某一天 不同月份取值也不同
getDay() 获取星期 取值为0~6;0表示星期日
getHours() 获取小时 取值为0~23
getMinutes() 获取分钟 取值为0~59
getSeconds() 获取秒 取值为0~59

其他方法:

const date = new Date();
console.log(date.toLocaleDateString()) //2023/12/3
console.log(date.toLocaleString()) // 2023/12/3 15:21:55
console.log(date.toLocaleTimeString()) // 15:22:01

案例:将当前时间以:YYYY-MM-DD HH:mm:ss形式显示

要求:

  1. 调用日期对象方法进行转换
  2. 记得数字要补0
<div></div>
<script>
    const div = document.querySelector('div');
    setInterval(function () {
        const date = new Date();
        const year = date.getFullYear();
        const month = date.getMonth() + 1 < 10 ? '0'+date.getMonth() + 1: date.getMonth() + 1;
        const day = date.getDate() < 10 ? '0'+date.getDate() : date.getDate();
        const hours = date.getHours() < 10 ? '0'+date.getHours() : date.getHours();
        const minutes = date.getMinutes() < 10 ? '0'+date.getMinutes() : date.getMinutes();
        const seconds = date.getSeconds() < 10 ? '0'+date.getSeconds() : date.getSeconds();
        div.innerHTML = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    },1000)
</script>

结果自己查看即可。

2.11.2 时间戳

时间戳是指1970年01月01日00时00分00秒起至现在的总秒数或毫秒数,它是一种特殊的计量时间的方式。

注:ECMAScript 中时间戳是以毫秒计的。

// 1. 实例化
const date = new Date()
// 2. 获取时间戳
console.log(date.getTime())
// 还有一种获取时间戳的方法
console.log(+new Date())
// 还有一种获取时间戳的方法
console.log(Date.now())

获取时间戳的方法,分别为 getTime()Date.now()+new Date()

其中Date.now()的方式只能获取当前时间戳,而前面两种方式可以获取指定事件的时间戳。

重点记住+new Date()的形式就可以了。

2.12 DOM 节点

回顾之前 DOM 的操作都是针对元素节点的属性或文本的,除此之外也有专门针对元素节点本身的操作,如插入、复制、删除、替换等。

DOM

节点分为3类:

  1. 元素节点:就是一些标签,如h1、a、input等标签
  2. 属性节点: 如id、class、value等属性值
  3. 文本节点:回车换行、标签中间夹的文本值

2.12.1 查找节点

DOM 树中的任意节点都不是孤立存在的,它们要么是父子关系,要么是兄弟关系,不仅如此,我们可以依据节点之间的关系查找节点

  • 父子关系

    父节点查找使用parentNode属性,返回最近一级父节点(DOM对象),找不到则返回null

    语法:子元素.parentNode

    <body>
    <div class="grandfather">
        <div class="father">
            <div class="son"></div>
        </div>
    </div>
    <script>
        //获取son节点
        const son = document.querySelector('.son');
        console.log(son);
        console.log(son.parentNode);
        console.log(son.parentNode.parentNode);
    </script>
    </body>
    

    结果

    可见通过拿到子节点,就可以访问父节点,并且返回的都是DOM对象,如果找不到,就返回null。


    子节点查找使用childNodes ,获取全部的子节点,包括属性节点和文本节点,回车换行会被认为是空白文本节点;children属性仅获取元素类型节点,两者返回的都是伪数组;其中在开发中我们使用较多的是children属性

    语法:父元素.children

    <ul>
        <li>hh</li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
    <script>
        //获取ul节点
        const ul = document.querySelector('ul');
        console.log(ul.children);
        console.log(ul.childNodes)
    </script>
    </body>
    

    结果

    可见children只会获取所有的元素节点,而childNodes获取了元素节点和文本节点。

  • 兄弟关系

    上一个兄弟:previousElementSibling属性;下一个兄弟:nextElementSibling属性

    从单词中就可以看出只会选择出元素节点

    <body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
    </ul>
    <script>
        //获取第二个li节点
        const li2 = document.querySelector('ul li:nth-child(2)');
        console.log(li2.previousElementSibling);
        console.log(li2.nextElementSibling);
    </script>
    </body>
    

    结果

    可见确实获取到了。

2.12.2 插入节点

在已有的 DOM 节点中插入新的 DOM 节点时,需要关注两个关键因素:首先要得到新的 DOM 节点,其次在哪个位置插入这个节点。

  1. 创建元素节点

    document.createElement('标签名')
    
  2. 追加节点

    新节点创建好了之后,要将其放入父元素中,才能在页面中显示出来;

    • 插入到父元素的最后一个子元素

      父元素.appendChild(要插入的元素)
      
    • 插入到父元素中某个子元素的前面

      父元素.insertBefore(要插入的元素,在哪个元素前面)
      

    注意:无引号

例如:

<body>
<ul>
    <li>noe</li>
    <li>two</li>
</ul>
<script>
    //创建新的li节点
    const three = document.createElement('li');
    three.innerHTML = 'three';
    console.log(three);
    //追加到父元素中最后一个子元素后面
    const father = document.querySelector('ul');
    father.appendChild(three);

    //创建一个新的li节点
    const zero = document.createElement('li');
    zero.innerHTML = 'zero';
    //追加到three的前面
    father.insertBefore(zero,three);
</script>
</body>

执行结果


在新增节点中,有一个特殊情况,那就是克隆节点。

语法:要克隆的目标元素.cloneNode(布尔值)

cloneNode会克隆出一个跟原标签一样的元素,括号内传入布尔值

  • 若为true,则代表克隆时会包含后代节点一起克隆
  • 若为false,则代表克隆时不包含后代节点
  • 默认为false

例如:

<body>
<ul>
    <li>noe</li>
    <li>two</li>
</ul>
<script>
    //克隆第一个li
    const ul = document.querySelector('ul');
    ul.appendChild(ul.children[0].cloneNode(true));
</script>
</body>

结果

2.12.3 删除节点

删除现有的 DOM 节点,也需要关注两个因素:首先由父节点删除子节点,其次是要删除哪个子节点。

要删除元素,必须通过父元素来删除。如果不存在父元素,则无法删除

语法:父元素.removeChild(要删除的元素)

例如:

<body>
<ul>
    <li>noe</li>
    <li>two</li>
</ul>
<script>
    //要删除节点,首先要获取父元素
    const ul = document.querySelector('ul');
    //删除第一个li
    ul.removeChild(ul.children[0]);
</script>
</body>

结果

2.13 小案例

考察前几节的学习情况。

我现在有一个表格,输入一些信息,点击录入,就可以录入表格中,并且能够实现删除:

<style>
    .contain {
        width: 1200px;
        height: 500px;
        background-color: white;
        margin: 0 auto;
    }
    .inputInfo {
        width: 1000px;
        height: 100px;
        margin: 0 auto;
    }

    input, select, option {
        width: 100px;
        border: 1px solid skyblue;
        margin-right: 26px;
        border-radius: 5px;
    }

    h1 {
        text-align: center;
    }

    .enter {
        background-color: skyblue;
        border: 0;
        border-radius: 5px;
    }

    table {
        border: 1px solid deepskyblue;
        width: 100%;
        height: 50px;
        border-collapse: collapse;
    }

    .top {
        background-color: skyblue;
        text-align: center;
    }

    .trContain {
        text-align: center;
    }

    .trContain:hover {
        background-color: gray;
    }

    th, td {
        border: 1px solid deepskyblue;
    }
</style>
<div class="contain">
    <div class="addRegion">
        <h1>新增学员</h1>
        <div class="inputInfo">
            姓名:<input type="text" class="username">
            年龄:<input type="text" class="age">
            性别:
            <select name="gender" id="gender" class="gender">
                <option value=""></option>
                <option value=""></option>
            </select>
            薪资:<input type="text" class="salary">
            就业城市:
            <select name="city" id="city" class="city">
                <option value="重庆">重庆</option>
            </select>
            <button class="enter">录入</button>
        </div>
    </div>
    <div class="list">
        <h1>就业榜</h1>
        <div class="inputInfo">
            <table>
                <tr class="top">
                    <th>学号</th>
                    <th>姓名</th>
                    <th>年龄</th>
                    <th>性别</th>
                    <th>薪资</th>
                    <th>就业城市</th>
                    <th>操作</th>
                </tr>
            </table>
        </div>
    </div>
</div>
<body>
<script>
    const username = document.querySelector('.username');
    const age = document.querySelector('.age');
    const gender = document.querySelector('.gender');
    const salary = document.querySelector('.salary');
    const city = document.querySelector('.city');
    const enter = document.querySelector('.enter');

    //声明一个空数组来存放信息对象
    const arr = [];
    //声明一个空信息对象
    const student = {
        stuid: '',
        username: '',
        age: '',
        gender: '',
        salary: '',
        city: '',
        op: `<a href="#" class="del">删除</a>`
    };

    //渲染到表格中
    const table = document.querySelector('table');
    //点击录入就将信息对象添加到数组中,然后渲染到页面中
    enter.addEventListener('click', function () {
        student.stuid = arr.length + 1;
        student.username = username.value;
        student.age = age.value;
        student.gender = gender.value;
        student.salary = salary.value;
        student.city = city.value;
        arr.push(student);

        //清空输入框的内容
        username.value = '';
        age.value = '';
        gender.value = '男';
        salary.value = '';
        city.value = '重庆';


        const tr = document.createElement('tr');
        tr.className = 'trContain';
        table.appendChild(tr);

        const attributeMapping = ['stuid', 'username', 'age', 'gender', 'salary', 'city', 'op']
        for (const attribute of attributeMapping) {
            const td = document.createElement('td');
            td.innerHTML = student[attribute];
            tr.appendChild(td);
        }
    });


    // 通过事件委托的方式来获取动态添加的数据,然后执行删除   这里删除的思想很重要,采用了事件委托的方式
    table.addEventListener('click', function (event) {
        //拿到a元素节点对象
        const aNode = event.target;
        // 检查点击的元素是否是 <a> 元素并且具有 'del' 类名
        if (aNode.tagName === 'A' && aNode.classList.contains('del')) {
            const dataId = aNode.getAttribute('data-id');
            // 在这里执行删除操作
            table.removeChild(aNode.parentNode.parentNode);
        }
    });
</script>
</body>

录入之前

录入结果

删除学号为1的学员

成功删除

2.14 BOM

BOM(Browser Object Model)是浏览器对象模型,也就是我们常用的window对象。

  • window对象是一个全局对象,也可以说是JavaScript中的顶级对象

  • 像document、alert()、console.log()这些都是window的属性,基本BOM的属性和方法都是window的。

  • 所有通过var定义在全局作用域中的变量、函数都会变成window对象的属性和方法

    const、let就不是了,是挂在了自己的作用域中。

  • window对象下的属性和方法调用的时候可以省略window

    document.querySelector() === window.document.querySelector()
    console.log(document === window.document) //true
    

2.15 定时器-延迟函数

注意此处区分一下间歇函数(setInterval)。

JavaScript 内置的一个用来让代码延迟执行的函数,叫 setTimeout;例如一个广告弹出,显示几秒钟关闭,就可用用到延迟执行函数。

语法:setTimeout(回调函数, 延迟时间)

setIntervalsetTimeout的区别:

  • setTimeout 仅仅只执行一次,所以可以理解为就是把一段代码延迟执行,平时省略window

  • 间歇函数 setInterval 每隔一段时间就执行一次,平时省略window

清除延时函数:clearTimeout(timerId)

注意:

  1. 延时函数需要等待,所以后面的代码会先执行
  2. 返回值是一个正整数,表示定时器的编号

2.16 JS执行机制

经典面试题:现在我有两段JS,分别给出输出的结果

console.log(111)
setTimeout(function (){
    console.log(222)
},1000)
console.log(333)
console.log(111)
setTimeout(function (){
    console.log(222)
},0)
console.log(333)

答案:两者都输出111、333、222

第一个输出132能够理解,因为setTimeout需要等待,让后面的代码先执行,但是第二个输出也是132就不能够理解了,因为他的等待时间为0,按道理应该是123才对,这里就要讲解一下JS的执行机制了。

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事

这是因为JavaScript这门脚本语言诞生的使命所致——JavaScript是为处理页面中用户的交互,以及操作 DOM而诞生的。比如我们对某个DOM元素进行添加和删除操作,不能同时进行。应该先进行添加,之后再删除。

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。这样所导致的问题是:如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

为了解决这个问题,利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程。于是,JS中出现了同步异步

  • 同步

    前一个任务结束后再执行后一个任务,程序的执行顺序与任务的排列顺序是一致的、同步的。比如做饭的同步做法:我们要烧水煮饭,等水开了(10分钟之后),再去切菜,炒菜。

  • 异步

    你在做一件事情时,因为这件事情会花费很长时间,在做这件事的同时,你还可以去处理其他事情。比如做饭的异步做法,我们在烧水的同时,利用这10分钟,去切菜,炒菜。

他们的本质区别:这条流水线上各个流程的执行顺序不同。

同步任务:同步任务都在主线程上执行,形成一个执行栈

同步任务

异步任务:JS的异步是通过回调函数实现的。一般而言,异步任务有以下三种类型:

  1. 普通事件,如click、resize等
  2. 资源加载,如load、error等
  3. 定时器,包括setIntervalsetTimeout等异步任务相关添加到任务队列中(任务队列也称为消息队列)。

异步任务

任务的执行流程:

  1. 先执行执行栈中的同步任务。
  2. 异步任务放入任务队列中。
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行

JS执行机制

由于主线程不断的重复获得任务、执行任务、再获取任务、再执行,所以这种机被称为**事件循环(event loop)**。

测试:

console.log(1)
document.addEventListener('click',function (){
    console.log(4)
})
console.log(3)
setTimeout(function (){
    console.log(2)
},3000)

执行结果为1324的时候:js会将13交给执行栈处理,24交给浏览器处理,当3秒过后,用户还没有点击,那么浏览器就会处理2,2处理完毕之后,就推到任务队列,执行栈就会去任务队列取出2,然后执行,最后输出2;所以就会导致1324;

执行结果为1342的时候就是用户在3秒内点击了,所以4就会先由浏览器执行,然会推到任务队列,最后交给执行栈处理。

2.17 location对象

location (地址) 它拆分并保存了 URL 地址的各个组成部分, 它是一个对象

常见属性和方法:

属性/方法 说明
href 属性,获取完整的 URL 地址,赋值时用于地址的跳转
search 属性,获取地址中携带的参数,符号 ?后面部分
hash 属性,获取地址中的啥希值,符号 # 后面部分
reload() 方法,用来刷新当前页面,传入参数 true 时表示强制刷新

案例:倒计时5秒后,自动跳转到百度

<body>
<a href="#"></a>
<script>
    const a = document.querySelector('a');
    let time = 5;
    let intervalId = setInterval(function () {
        if (time === 0) {
            location.href = 'https://www.baidu.com'; //赋值并跳转
            clearInterval(intervalId);
        }
        a.innerHTML = `${time--}s后自动跳转到百度`
    },1000);
</script>
</body>

测试成功!

search方法:

<body>
<form action="#">
    <input type="text" name="username">
    <input type="text" name="age">
    <input type="submit">
</form>
<script>
    console.log(location.search);
</script>
</body>

测试成功

hash方法:

<body>
<a href="#/one">one</a>
<a href="#/two">two</a>
<a href="#/three">three</a>
<script>
    console.log(location.hash);
</script>
</body>

测试成功

2.18 navigator对象

navigator是对象,该对象下记录了浏览器自身的相关信息

常用属性和方法:

  • 通过 userAgent 检测浏览器的版本及平台

查看一下navigator对象:console.log(navigator);

结果中的userAgent属性

现在我有一个需求,要求当检测设备时移动端的时候,跳转到移动端的网页,是PC端的时候跳转到PC端的网页:

需要的效果:

PC端

移动端

可见二者呈现的形式是不同的。

js实现:

//&nbsp;检测&nbsp;userAgent(浏览器信息)
(function&nbsp;()&nbsp;{
  const&nbsp;userAgent&nbsp;=&nbsp;navigator.userAgent
  //&nbsp;验证是否为Android或iPhone
  const&nbsp;android&nbsp;=&nbsp;userAgent.match(/(Android);?[\s\/]+([\d.]+)?/)
  const&nbsp;iphone&nbsp;=&nbsp;userAgent.match(/(iPhone\sOS)\s([\d_]+)/)
  //&nbsp;如果是Android或iPhone,则跳转至移动站点
  if&nbsp;(android&nbsp;||&nbsp;iphone)&nbsp;{
    location.href&nbsp;=&nbsp;'http://m.itcast.cn'
  }})();

2.19 histroy对象

history (历史)是对象,主要管理历史记录, 该对象与浏览器地址栏的操作相对应,如前进、后退等

主要是这个前进和回退

常用的属性和方法

方法 作用
back() 回退
forward() 前进
go(参数) 前进回退功能,参数如果是1前进一个页面,-1回退一个页面

使用场景

history对象一般在实际开发中比较少用,但是会在一些OA 办公系统中见到。

2.20 本地存储

本地存储:将数据存储在本地浏览器中

常见的使用场景:

https://todomvc.com/examples/vanilla-es6/ 页面刷新数据不丢失

好处:

1、页面刷新或者关闭不丢失数据,实现数据持久化

2、容量较大,sessionStorage和 localStorage 约 5M 左右

2.20.1 localStorage(重点)

作用: 数据可以长期保留在本地浏览器中,刷新页面和关闭页面,数据也不会丢失

特性:以键值对的形式存储,并且存储的是字符串, 省略了window

语法

2.20.2 sessionStorage(了解)

特性:

  • 用法跟localStorage基本相同
  • 区别是:当页面浏览器被关闭时,存储在 sessionStorage 的数据会被清除

存储:sessionStorage.setItem(key,value)

获取:sessionStorage.getItem(key)

删除:sessionStorage.removeItem(key)

2.20.3 localStorage 存储复杂数据类型

问题:本地只能存储字符串,无法存储复杂数据类型.

解决:需要将复杂数据类型转换成 JSON字符串,在存储到本地

语法:JSON.stringify(复杂数据类型)

JSON字符串:

  • 首先是1个字符串
  • 属性名使用双引号引起来,不能单引号
  • 属性值如果是字符串型也必须双引号

例如:

const obj = {
    name: '念心卓',
    age: 18
}
localStorage.setItem("obj",obj);

无法看见具体内容,只有Object

包括取出来也是:

其实就是因为localStorage只能存储字符串,而我们上面是直接存的对象,如果把对象变为字符串不就行了,这就是我们说的JSON字符串。

解决:

  • 存入的时候将对象变为JSON字符串:JSON.stringify(复杂数据类型)
  • 取出的时候将JSON字符串变为对象:JSON.parse(JSON字符串)
const obj = {
    name: '念心卓',
    age: 18
}
//存入
localStorage.setItem("obj",JSON.stringify(obj));
console.log(JSON.parse(localStorage.getItem('obj')));

成功

成功

如果存入的时候转为了字符串,但是取出的时候没有转为对象:

结果

可见还是有区别的。

2.21 正则表达式

正则表达式(Regular Expression)是用于匹配字符串中字符组合的模式。在JavaScript中,正则表达式也是对象,通常用来查找、替换那些符合正则表达式的文本,许多语言都支持正则表达式。

使用场景:

  • 例如验证表单:手机号表单要求用户只能输入11位的数字 (匹配)
  • 过滤掉页面内容中的一些敏感词(替换),或从字符串中获取我们想要的特定部分(提取)等

67607966636

2.21.1 正则基本使用

步骤:

  1. 定义规则

    const reg =  /表达式/
    

    其中/ /正则表达式字面量,并且里面不需要引号,你写什么,就查找什么。

    正则表达式也是对象

  2. 使用正则

    test()方法 用来查看正则表达式与指定的字符串是否匹配

    如果正则表达式与指定的字符串匹配 ,返回true,否则false

// 正则表达式的基本使用
const str = 'web前端开发'
// 1. 定义规则
const reg = /web/

// 2. 使用正则  test()
console.log(reg.test(str))  // true  如果符合规则匹配上则返回true
console.log(reg.test('java开发'))  // false  如果不符合规则匹配上则返回 false

结果

2.21.2 元字符

普通字符:

  • 大多数的字符仅能够描述它们本身,这些字符称作普通字符,例如所有的字母和数字。
  • 普通字符只能够匹配字符串中与它们相同的字符。
  • 比如,规定用户只能输入英文26个英文字母,普通字符的话 /[abcdefghijklmnopqrstuvwxyz]/

元字符(特殊字符)

  • 是一些具有特殊含义的字符,可以极大提高了灵活性和强大的匹配功能。
  • 比如,规定用户只能输入英文26个英文字母,换成元字符写法: /[a-z]/

为了方便记忆和学习,我们对众多的元字符进行了分类:

  • 边界符(表示位置,开头和结尾,必须用什么开头,用什么结尾)
  • 量词(表示重复次数)
  • 字符类(比如\d表示0~9)

2.21.3 边界符

正则表达式中的边界符(位置符)用来提示字符所处的位置,主要有两个字符:

边界符 说明
^ 表示匹配行首的文本(以谁开始)
$ 表示匹配行尾的文本(以谁结束)

如果 ^ 和 $ 在一起,表示必须是精确匹配

console.log(/哈/.test('二哈'));//true
console.log(/^哈/.test('二哈'));//false; 表示必须要以哈开头
console.log(/^哈二/.test('哈二哈'));//true 
console.log(/^哈$/.test('二哈'));//false;表示精确匹配,必须是哈
console.log(/^哈$/.test('哈'));//true

2.21.4 量词

量词用来设定某个模式重复次数

量词 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

注意: 逗号左右两侧千万不要出现空格

<body>
  <script>
    // 元字符之量词
    // 1. * 重复次数 >= 0 次
    const reg1 = /^w*$/
    console.log(reg1.test(''))  // true
    console.log(reg1.test('w'))  // true
    console.log(reg1.test('ww'))  // true
    console.log('-----------------------')

    // 2. + 重复次数 >= 1 次
    const reg2 = /^w+$/
    console.log(reg2.test(''))  // false
    console.log(reg2.test('w'))  // true
    console.log(reg2.test('ww'))  // true
    console.log('-----------------------')

    // 3. ? 重复次数  0 || 1 
    const reg3 = /^w?$/
    console.log(reg3.test(''))  // true
    console.log(reg3.test('w'))  // true
    console.log(reg3.test('ww'))  // false
    console.log('-----------------------')


    // 4. {n} 重复 n 次
    const reg4 = /^w{3}$/
    console.log(reg4.test(''))  // false
    console.log(reg4.test('w'))  // flase
    console.log(reg4.test('ww'))  // false
    console.log(reg4.test('www'))  // true
    console.log(reg4.test('wwww'))  // false
    console.log('-----------------------')

    // 5. {n,} 重复次数 >= n 
    const reg5 = /^w{2,}$/
    console.log(reg5.test(''))  // false
    console.log(reg5.test('w'))  // false
    console.log(reg5.test('ww'))  // true
    console.log(reg5.test('www'))  // true
    console.log('-----------------------')

    // 6. {n,m}   n =< 重复次数 <= m
    const reg6 = /^w{2,4}$/
    console.log(reg6.test('w'))  // false
    console.log(reg6.test('ww'))  // true
    console.log(reg6.test('www'))  // true
    console.log(reg6.test('wwww'))  // true
    console.log(reg6.test('wwwww'))  // false

    // 7. 注意事项: 逗号两侧千万不要加空格否则会匹配失败

  </script>

2.21.5 字符类

某些常见模式的简写方式,区分字母和数字

字符类 说明
\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]
\S 匹配非空格(包括换行符、制表符、空格符等),相等于[^\t\r\n\v\f]

日期格式:/^\d{4}-\d{1,2}-\d{1,2}$/

2.21.6 范围

表示字符的范围,定义的规则限定在某个范围,比如只能是英文字母,或者数字等等,用表示范围

<body>
  <script>
    // 元字符之范围  []  
    // 1. [abc] 匹配包含的单个字符, 多选1
    const reg1 = /^[abc]$/
    console.log(reg1.test('a'))  // true
    console.log(reg1.test('b'))  // true
    console.log(reg1.test('c'))  // true
    console.log(reg1.test('d'))  // false
    console.log(reg1.test('ab'))  // false

    // 2. [a-z] 连字符 单个
    const reg2 = /^[a-z]$/
    console.log(reg2.test('a'))  // true
    console.log(reg2.test('p'))  // true
    console.log(reg2.test('0'))  // false
    console.log(reg2.test('A'))  // false
    // 想要包含小写字母,大写字母 ,数字
    const reg3 = /^[a-zA-Z0-9]$/
    console.log(reg3.test('B'))  // true
    console.log(reg3.test('b'))  // true
    console.log(reg3.test(9))  // true
    console.log(reg3.test(','))  // flase

    // 用户名可以输入英文字母,数字,可以加下划线,要求 6~16位
    const reg4 = /^[a-zA-Z0-9_]{6,16}$/
    console.log(reg4.test('abcd1'))  // false 
    console.log(reg4.test('abcd12'))  // true
    console.log(reg4.test('ABcd12'))  // true
    console.log(reg4.test('ABcd12_'))  // true

    // 3. [^a-z] 取反符
    const reg5 = /^[^a-z]$/
    console.log(reg5.test('a'))  // false 
    console.log(reg5.test('A'))  // true
    console.log(reg5.test(8))  // true

  </script>
</body>

2.22.7 替换和修饰符

replace 替换方法,可以完成字符的替换:字符串.replace(/正则表达式/,'替换的文本')

<body>
  <script>
    // 替换和修饰符
    const str = '欢迎大家学习前端,相信大家一定能学好前端,都成为前端大神'
    // 1. 替换  replace  需求:把前端替换为 web
    // 1.1 replace 返回值是替换完毕的字符串
    // const strEnd = str.replace(/前端/, 'web') 只能替换一个
  </script>
</body>

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

  • i 是单词 ignore 的缩写,正则匹配时字母不区分大小写
  • g 是单词 global 的缩写,匹配所有满足正则表达式的结果
<body>
  <script>
    // 替换和修饰符
    const str = '欢迎大家学习前端,相信大家一定能学好前端,都成为前端大神'
    // 1. 替换  replace  需求:把前端替换为 web
    // 1.1 replace 返回值是替换完毕的字符串
    // const strEnd = str.replace(/前端/, 'web') //只能替换一个

    // 2. 修饰符 g 全部替换
    const strEnd = str.replace(/前端/g, 'web') //替换所有
    console.log(strEnd) 
  </script>
</body>

3. 进阶

3.1 作用域

作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。

3.3.1 局部作用域

局部作用域分为函数作用域块作用域

  1. 函数作用域

    在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。

    <script>
      // 声明 counter 函数
      function counter(x, y) {
        // 函数内部声明的变量
        const s = x + y
        console.log(s) // 18
      }
      // 设用 counter 函数
      counter(10, 8)
      // 访问变量 s
      console.log(s)// 报错
    </script>
    

    总结:

    1. 函数内部声明的变量,在函数外部无法被访问
    2. 函数的参数也是函数内部的局部变量
    3. 不同函数内部声明的变量无法互相访问
    4. 函数执行完毕后,函数内部的变量实际被清空了
  2. 块作用域

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

    <script>
      {
        // age 只能在该代码块中被访问
        let age = 18;
        console.log(age); // 正常
      }
      
      // 超出了 age 的作用域
      console.log(age) // 报错
      
      let flag = true;
      if(flag) {
        // str 只能在该代码块中被访问
        let str = 'hello world!'
        console.log(str); // 正常
      }
      
      // 超出了 age 的作用域
      console.log(str); // 报错
      
      for(let t = 1; t <= 6; t++) {
        // t 只能在该代码块中被访问
        console.log(t); // 正常
      }
      
      // 超出了 t 的作用域
      console.log(t); // 报错
    </script>
    

    JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。

    <script>
      // 必须要有值
      const version = '1.0.0';
    
      // 不能重新赋值
      // version = '1.0.1';
    
      // 常量值为对象类型
      const user = {
        name: '小明',
        age: 18
      }
    
      // 不能重新赋值
      user = {};
    
      // 属性和方法允许被修改
      user.name = '小小明';
      user.gender = '男';
    </script>
    

    总结:

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

    注:开发中 letconst 经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const 声明成常量。

3.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>

总结:

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

JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。

3.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. 嵌套关系的作用域串联起来形成了作用域链
  2. 相同作用域链中按着从小到大的规则查找变量
  3. 子作用域能够访问父作用域,父级作用域无法访问子级作用域

3.1.4 垃圾回收机制

垃圾回收机制(Garbage Collection)简称GC

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

**内存的生命周期 **:JS环境中分配的内存,一般有如下生命周期:

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

说明:

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

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

堆栈空间分配区别:

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

下面介绍两种常见的浏览器垃圾回收算法:引用计数法和标记清除法

引用计数法:

IE采用的用计数算法,定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有用了就回收对象

算法:

  1. 跟踪记录被引用的次数
  2. 如果被引用了一次,那么就记录次数1,多次引用会累加++
  3. 如果减少一个引用就减1 –
  4. 如果引用次数是0,则释放内存

标记清除法:

现代的浏览器已经不再使用引用计数算法了。

现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。

核心:

  1. 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
  2. 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。
  3. 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

3.1.4 闭包

概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域简单理解:闭包=内层函数+外层函数的变量

闭包是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数。

如下代码所示:

function outer() {
  const a = 1
  function f() { //内层函数
    console.log(a) //使用外部变量
  }
  f()
}
outer()

所以,闭包的基本格式:

function outer() {
  const a = 1
  function fn() {
    console.log(a)
  }
  return fn //返回的是fn整个函数,不是返回值
}
const fun = outer() //实际就是将fn整个函数赋值给了fun,所以fun也是函数
fun()//调用

使用闭包实现数据私有:

我现在有一个需求,要求我统计调用函数的次数:

let count = 0;
function fn(){
    console.log(`fn函数执行了${++count}次`)
}

结果

可见当我修改了count的值之后,函数执行本身没问题,但是执行的次数却不对了,出现这个原因就是因为count是一个全局变量,容易被修改,所以,为了保护数据,实现数据私有,我们需要使用闭包来进行:

function fn(){
    let count = 0;
    function gn(){
        console.log(`fn函数执行了${++count}次`)
    }
    return gn;
}
const kn = fn();
kn();

结果

可见count被私有化了,外部无法直接修改count

总结:

1.怎么理解闭包?

  • 闭包 = 内层函数 + 外层函数的变量

2.闭包的作用?

  • 封闭数据,实现数据私有,外部也可以访问函数内部的变量
  • 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来

3.闭包可能引起的问题?

  • 内存泄漏

3.1.5 变量提升

变量提升是JavaScript中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)

// 访问变量 str
console.log(str + 'world!'); //输出undifinedworld
// 声明变量 str
var str = 'hello ';

原因:变量提升的时候,会检测当前作用域下所有以var声明的变量,然后提到这个变量使用之前(但是还没赋值)。

总结:

  1. 变量在未声明即被访问时会报语法错误
  2. 变量在声明之前即被访问,变量的值为 undefined
  3. let 声明的变量不存在变量提升,推荐使用 let
  4. 变量提升出现在相同作用域当中
  5. 实际开发中推荐先声明再访问变量

注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let 可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料

当然,也有函数提升,但是函数表达式没有提升。

3.2 函数

3.2.1 函数提升

函数提升与变量提升比较类似,是指函数在声明之前即可被调用。

<script>
  // 调用函数
  foo()
  // 声明函数
  function foo() {
    console.log('声明之前即被调用...')
  }

  // 不存在提升现象
  bar()  // 错误
  var bar = function () {
    console.log('函数表达式不存在提升现象...')
  }
</script>

总结:

  1. 函数提升能够使函数的声明调用更灵活
  2. 函数表达式不存在提升的现象
  3. 函数提升出现在相同作用域当中

3.2.2 函数参数

函数参数的使用细节,能够提升函数应用的灵活度。

  • 默认值

    <script>
      // 设置参数默认值
      function sayHi(name="小明", age=18) {
        document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
      }
      // 调用函数
      sayHi();
      sayHi('小红');
      sayHi('小刚', 21);
    </script>
    

    总结:

    1. 声明函数时为形参赋值即为参数的默认值
    2. 如果参数未自定义默认值时,参数的默认值为 undefined
    3. 调用函数时没有传入对应实参时,参数的默认值被当做实参传入
  • 动态参数

    当不确定传入多少实参的时候,动态参数就会起作用。

    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>
    

    总结:

    1. arguments 是一个伪数组
    2. arguments 的作用是动态获取函数的实参
  • 剩余参数

    有点像Java语法中的,不定长数组

    <script>
      function config(baseURL, ...other) {
        console.log(baseURL) // 得到 'http://baidu.com'
        console.log(other)  // other  得到 ['get', 'json']
      }
      // 调用函数
      config('http://baidu.com', 'get', 'json');
    </script>
    

    总结:

    1. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参
    2. 借助 ... 获取的剩余实参,是个真数组

在开发中,提倡使用剩余参数

其实看到这里,你会发现,剩余参数与展开运算符有些相似。

  • 展开运算符

    展开运算符(...),将一个数组进行展开:

    const arr = [1,2,3,4,5];
    console.log(...arr) // ...就是展开运算符
    

    执行结果

    典型应用:求数组最大值、最小值、合并数组等

    const arr = [1,2,3,4,5];
    //求最大值
    //没有展开运算符的时候,我们只能这样求最大值,因为Math.max中不能写数组
    console.log(Math.max(1,2,3,4,5)) 
    console.log(Math.max(...arr))
    

    执行结果

    const arr1 = [1,2,3,4,5];
    //合并两个数组
    const arr2 = [7,8,9];
    const newArr = [...arr1,...arr2];
    console.log(newArr);
    

    执行结果

    注意:展开运算符不会修改原数组

3.2.3 箭头函数

箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上

目的:引入箭头函数的目的是更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁

使用场景:箭头函数更适用于那些本来需要匿名函数的地方

基本语法:

 //以前函数表达式写法
const fn1 = function () {
    console.log(123)
}
//调用
fn1()
//箭头函数
const fn2 = () => {
    console.log(123)
}
//调用
fn2()

只有一个形参的时候,可以省略小括号:

const fn = x => { //省略了小括号
    console.log(x)
}
fn(1)

只有一行代码的时候,我们可以省略大括号:

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

只有一行代码的时候,可以省略return:

const fn = x => x + x
console.log(fn(1))

箭头函数可以直接返回一个对象:

const fn = (uname) => ({ uname: uname }) //注意这里外层是圆括号,目的是为了区分对象的大括号
console.log(fn('刘德华'))

总结:

  1. 箭头函数属于表达式函数,因此不存在函数提升
  2. 箭头函数只有一个参数时可以省略圆括号 ()
  3. 箭头函数函数体只有一行代码时可以省略花括号 {},并自动做为返回值被返回

熟悉Java的同学你就知道,其实就和Java中的lambda表达式差不多,不过Java中的lambda表达式是在JavaScript之后才引入的。

  • 箭头函数参数

    普通函数有arguments动态参数,但是箭头函数没有arguments动态参数,但是有剩余参数…args

    // 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
    
  • 箭头函数 this

    在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值,非常令人讨厌,以前:

    //this指向的初略规则:随调用指向谁
    console.log(this) //window
    function fn() {
        console.log(this) //window
    }
    fn()  //因为这里的
    

    箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this,现在:

    const fn1 = () => console.log(this) //这个this指向window
    fn1();
    
    const obj1 = {
        uname:'nxz',
        sayHi:function () {
            console.log(this) //这个this指向obj,因为谁调用指向谁
        }
    }
    obj1.sayHi();
    
    const obj2 = {
        uname:'nxz',
        sayHi:() => {
            console.log(this) //这个this指向 window
        }
    }
    obj2.sayHi()
    
    const obj3 = {
        uname:'nxz',
        sayHi:function (){
            const x = () => {
                console.log(this) //这个this指向 obj
            }
            x()
        }
    }
    obj3.sayHi()
    

    结果

在开发中【使用箭头函数前需要考虑函数中this的值】,事件回调函数使用箭头函数时,this为全局的window,因此 DOM事件回调函数为了简便,还是不太推荐使用箭头函数

3.3 解构赋值

解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。

3.3.1 数组解构

数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法。

基本语法:

  1. 赋值运算符=左侧的[]用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
  2. 变量的顺序对应数组单元值的位置依次进行赋值操作
// 普通的数组
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]
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

典型应用:交换两个变量

let a = 1
let b = 2; //这里的分号是必须加上
[b,a] = [a,b]
console.log(a,b) //输出 2 1

注意:JavaScript中必须加分号的情况

  1. 多个立即执行函数的后面必须加上分号
  2. 数组结构

变量的数量大于单元值数量时,多余的变量将被赋值为 undefined

const [a,b,c,d] = [1,2,3];
console.log(a,b,c,d) //1 2 3 undefined

变量的数量小于单元值数量时,可以通过 ... 获取剩余单元值,但只能置于最末位

const [a,b,...c] = [1,2,3,4,5];
console.log(a,b,c) //1 2 [3,4,5]

允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效

const [a = 1, b = 2, c = 0] = [8, 8]
console.log(a, b, c) //8 8 0

总结:

  1. 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
  2. 变量的顺序对应数组单元值的位置依次进行赋值操作
  3. 变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
  4. 变量的数量小于单元值数量时,可以通过 ... 获取剩余单元值,但只能置于最末位
  5. 允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效

注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析

3.3.2 对象解构

对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法。

基本语法:

  1. 赋值运算符=左侧的{}用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  2. 对象属性的值将被赋值给与属性名相同的变量
  3. 注意解构的变量名不要和外面的变量名冲突否则报错
  4. 对象中找不到与变量名一致的属性时变量值为undefined
//如果外部有声明过uname或者age,下面代码就会报错
const {uname, age, hight} = {uname: 'nxz', age: 18, weight: '60kg'}
console.log(uname, age, hight) //nxz 18 undefined

如果在使用对象解构之前,确实声明了相同的变量,那么我们就只能给对象解构中的变量名进行重新赋值,重新赋值语法:旧变量名:新变量名

const uname = '念心卓'
const {uname: username, age, hight} = {uname: 'nxz', age: 18, weight: '60kg'}
console.log(username, age, hight) //nxz 18 undefined

数组对象解构也是一样的道理:

const [{uname, age}] = [{
    uname: 'nxz',
    age: 18
}]
console.log(uname, age) //nxz 18

总结:

  1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  2. 对象属性的值将被赋值给与属性名相同的变量
  3. 对象中找不到与变量名一致的属性时变量值为 undefined
  4. 允许初始化变量的默认值,属性不存在或单元值为 undefined 时默认值才会生效

注:支持多维解构赋值

// 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)

3.4 深入对象

3.4.1 创建对象的三种方式

  1. 利用对象字面量创建对象

    const o = {
        name:'nxz'
    }
    
  2. 利用new Object创建对象

    const o = new Object();
    console.log(o);
    o.uname = 'nxz';
    console.log(o);
    

    或者:

    const o = new Object({uname:'nxz'});
    console.log(o);
    
  3. 利用构造函数创建对象

3.4.2 构造函数

构造函数:是一种特殊的函数,主要用来初始化对象

使用场景:常规的{…}语法允许创建一个对象。比如我们创建了佩奇的对象,继续创建乔治的对象还需要重新写一遍,此时可以通过构造函数来快速创建多个类似的对象。

构造函数和字面量创建对象展示

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以大写字母开头
  2. 它们只能由”new“操作符来执行

总结:

  1. 使用 new 关键字调用函数的行为被称为实例化

  2. 实例化构造函数时没有参数时可以省略 ()

  3. 构造函数的返回值即为新创建的对象,构造函数中无需写return,就算写了也无效

  4. new Object ()、new Date()也是实例化构造函数

注:实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。

3.4.3 实例成员

通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员(实例属性和实例方法)

// 构造函数
function Person() {
    // 构造函数内部的 this 就是实例对象
    // 实例对象中动态添加属性
    this.name = '小明'
    // 实例对象动态添加方法
    this.sayHi = function () {
      console.log('大家好~')
    }
}
// 实例化,p1 是实例对象
// p1 实际就是 构造函数内部的 this
const p1 = new Person()
console.log(p1)
console.log(p1.name) // 访问实例属性
p1.sayHi() // 调用实例方法

总结:

  1. 构造函数内部 this 实际上就是实例对象,为其动态添加的属性和方法即为实例成员
  2. 为构造函数传入参数,动态创建结构相同但值不同的对象

注:构造函数创建的实例对象彼此独立互不影响。

3.4.4 静态成员

在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员

// 构造函数
function Person(name, age) {
    // 省略实例成员
}
// 静态属性
Person.eyes = 2
Person.arms = 2
// 静态方法
Person.walk = function () {
    console.log('^_^人都会走路...')
    // this 指向 Person
    console.log(this.eyes)
}

总结:

  1. 静态成员指的是添加到构造函数本身的属性和方法
  2. 一般公共特征的属性或方法静态成员设置为静态成员
  3. 静态成员方法中的 this 指向构造函数本身

3.5 内置构造函数

在 JavaScript 中最主要的数据类型有 6 种,分别是字符串、数值、布尔、undefined、null 和 对象,常见的对象类型数据包括数组和普通对象。

  • 基本数据类型:字符串、数值、布尔、undefined、null

  • 引用数据类型:对象

在 JavaScript 内置了一些构造函数,绝大部的数据处理都是基于这些构造函数实现的,JavaScript 基础阶段学习的 Date 就是内置的构造函数。

其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型,这就是为什么我们基本数据类型也可以使用属性,例如:

const str = 'abcd';
console.log(str.length) //4 这里就是调用了包装类型的属性,底层是 const str = new String();

JavaScript中几乎所有的数据都可以基于构成函数创建。

3.5.1 Object

Object是内置的构造函数,用于创建普通对象。

const user = new Object({name:'小明',age:15})

推荐使用字面量方式声明对象,而不是Object构造函数,不过构造函数也为我们提供了一些好用的方法。

以下有3个静态方法:

  1. Object.keys静态方法获取对象中所有属性(键)

    我们有这么一个对象:

    const obj = {name:'小明',age:15};
    

    以前要想全部把里面的属性获取出来有两种方式:

    //方式1 使用对象.属性来获取
    //方式2
    for(let k in o){
        console.log(k); //获取属性 name  age
        console.log(o[k]); //获取属性值 小明 15
    }
    

    然后现在我们有了Object.keys,就不用这么麻烦了

    const obj = {name:'小明',age:15};
    const arr = Object.keys(obj);
    console.log(arr); // ['name','age']
    

    注意:返回的是一个新数组

    类似的,还有Object.values获取所有的属性值

  2. Object.assign静态方法常用于对象拷贝

    const obj = {name:'小明',age:15};
    const newObj = {};
    Object.assign(newObj,obj) //新对象,原对象
    console.log(newObj); //{name:'小明',age:15}
    

    经常使用在给对象添加属性的地方;

    const obj = {name:'小明',age:15};
    Object.assign(obj,{gender:'男'}) //新对象,原对象
    console.log(obj); //{name:'小明',age:15,gender:'男'}
    
  3. Object.values静态方法获取对象中所有的属性值

总结:

  1. 推荐使用字面量方式声明对象,而不是 Object 构造函数
  2. Object.assign 静态方法创建新的对象
  3. Object.keys 静态方法获取对象中所有属性
  4. Object.values 表态方法获取对象中所有属性值

3.5.2 Array

Array 是内置的构造函数,用于创建数组。

// 构造函数创建数组
let arr = new Array(5, 7, 8);

// 字面量方式创建数组
let list = ['html', 'css', 'javascript']

数组赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。

实例方法:

方法 作用 说明
forEach 遍历数组 不返回数组,经常用于查找遍历数组元素
filter 过滤数组 返回新数组,返回的是筛选满足条件的数组元素
map 迭代数组 返回新数组,返回的是处理之后的数组元素,想要使用返回的新数组
reduce 累加器 返回累计处理的结果,经常用于求和等

reduce基本语法:

arr.reduce(function(){},起始值);
//或者
arr.reduce(function(上一次值,当前值){},起始值);

总结:

  1. 推荐使用字面量方式声明数组,而不是 Array 构造函数

  2. 实例方法 forEach 用于遍历数组,替代 for 循环 (重点)

  3. 实例方法 filter 过滤数组单元值,生成新数组(重点)

  4. 实例方法 map 迭代原数组,生成新数组(重点)

  5. 实例方法 join 数组元素拼接为字符串,返回字符串(重点)

  6. 实例方法 find 查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)

  7. 实例方法every 检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)

  8. 实例方法some 检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false

  9. 实例方法 concat 合并两个数组,返回生成新数组

  10. 实例方法 sort 对原数组单元值排序

  11. 实例方法 splice 删除或替换原数组单元

  12. 实例方法 reverse 反转数组

  13. 实例方法 findIndex 查找元素的索引值

总结中这些实例方法的使用,可以自己去查看MDN。

3.5.3 包装类型

在 JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法,如下代码举例:

// 字符串类型
const str = 'hello world!'
// 统计字符的长度(字符数量)
console.log(str.length)

// 数值类型
const price = 12.345
// 保留两位小数
price.toFixed(2) // 12.34

之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被称为包装类型

3.5.4 String

String 是内置的构造函数,用于创建字符串。

// 使用构造函数创建字符串
let str = new String('hello world!');

// 字面量创建字符串
let str2 = '你好,世界!';

// 检测是否属于同一个构造函数
console.log(str.constructor === str2.constructor); // true
console.log(str instanceof String); // false

总结:

  1. 实例属性 length 用来获取字符串的度长(重点)
  2. 实例方法 split('分隔符') 用来将字符串拆分成数组(重点)
  3. 实例方法 substring(需要截取的第一个字符的索引[,结束的索引号]) 用于字符串截取(重点)
  4. 实例方法 startsWith(检测字符串[, 检测位置索引号]) 检测是否以某字符开头(重点)
  5. 实例方法 includes(搜索的字符串[, 检测位置索引号]) 判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点)
  6. 实例方法 toUpperCase 用于将字母转换成大写
  7. 实例方法 toLowerCase 用于将就转换成小写
  8. 实例方法 indexOf 检测是否包含某字符
  9. 实例方法 endsWith 检测是否以某字符结尾
  10. 实例方法 replace 用于替换字符串,支持正则匹配
  11. 实例方法 match 用于查找字符串,支持正则匹配

注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。

同样的,具体的使用可以查看MDN。

3.5.5 Number

Number 是内置的构造函数,用于创建数值。

// 使用构造函数创建数值
let x = new Number('10')
let y = new Number(5)

// 字面量创建数值
let z = 20

总结:

  1. 推荐使用字面量方式声明数值,而不是 Number 构造函数

  2. 实例方法 toFixed 用于设置保留小数位的长度

    const price = 12.234;
    console.log(price.toFixed(2)) //12.23
    

具体使用可以查看MDN。

3.6 编程思想

3.6.1 面向过程

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了。

举个栗子:蛋炒饭

3.6.2 面向对象

面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。

面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。

面向对象的特性:

  • 封装性

  • 继承性

  • 多态性

学过Java的同学一看就懂。这里就不多说了,其中JavaScript是通过构造函数来实现的封装。

3.7 原型对象

原型主要就是用来解决构造函数的浪费内存问题。

  • 构造函数通过原型分配的函数是所有对象所共享的

  • JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象(Star.prototype);其中Star是构造函数,prototype 是属性,Star.prototype返回的就是一个原型对象。

  • 原型对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存

  • 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法

  • 构造函数和原型对象中的this 都指向 实例化的对象

function Person() {
    
}
// 每个函数都有 prototype 属性
console.log(Person.prototype) //输出的是一个原型对象

了解了 JavaScript 中构造函数与原型对象的关系后,再来看原型对象具体的作用,如下代码所示:

function Person() {
// 此处未定义任何方法
}

// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
    console.log('Hi~');
}

// 实例化
let p1 = new Person();
p1.sayHi(); // 输出结果为 Hi~

构造函数 Person 中未定义任何方法,这时实例对象调用了原型对象中的方法 sayHi,接下来改动一下代码:

function Person() {
    // 此处定义同名方法 sayHi
    this.sayHi = function () {
      console.log('嗨!');
    }
}

// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
    console.log('Hi~');
}
let p1 = new Person();
p1.sayHi(); // 输出结果为 嗨!

构造函数 Person 中定义与原型对象中相同名称的方法,这时实例对象调用则是构造函中的方法 sayHi

通过以上两个简单示例不难发现 JavaScript 中对象的工作机制:当访问对象的属性或方法时,先在当前实例对象查找,然后再去原型对象查找,并且原型对象被所有实例共享。

function Person() {
    // 此处定义同名方法 sayHi
    this.sayHi = function () {
      console.log('嗨!' + this.name)
    }
}
// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
    console.log('Hi~' + this.name)
}
// 在构造函数的原型对象上添加属性
Person.prototype.name = '小明'

let p1 = new Person()
p1.sayHi(); // 输出结果为 嗨!

let p2 = new Person()
p2.sayHi()  // 输出结果为 嗨!

总结:结合构造函数原型的特征,实际开发重往往会将封装的功能函数添加到原型对象中。

所以,原型是一个对象,我们也称为prototype为原型对象,原型对象的作用为:共享方法、将那些不变的方法直接定义在prototype对象上。

3.7.1 constructor 属性

在哪里? 每个原型对象里面都有个constructor 属性(constructor 构造函数)

作用:该属性指向该原型对象的构造函数, 简单理解,就是指向我的爸爸,我是有爸爸的孩子

注意这里的关系:构造函数中有原型对象,原型对象中有构造函数。

关系

描述:构造函数中有原型对象,也就是上面的箭头,原型对象中,有constructor 属性,又能够指回构造函数,二者是相互指向的。

function Star(){
    console.log(123)
}
console.log(Star.prototype.constructor === Star) //输出true

使用场景:

如果有多个对象的方法,我们可以给原型对象采取对象形式赋值:

function Star(){
    console.log(123)
}
Star.prototype = {
    sayHi:function () {
        console.log("Hi");
    },
    sayHiWithChinese:function () {
        console.log("嗨");
    }
}
console.log(Star.prototype)

可见可以一次性赋值,但是constructor 属性不见了

但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了

console.log(Star.prototype.constructor === Star) //false

此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数:

function Star(){
    console.log(123)
}
Star.prototype = {
    //重新赋值后,要重新指回构造函数
    constructor:Star,
    sayHi:function () {
        console.log("Hi");
    },
    sayHiWithChinese:function () {
        console.log("嗨");
    }
}
console.log(Star.prototype)
console.log(Star.prototype.constructor === Star)

可见成功

思考:

构造函数可以创建实例对象,构造函数还有一个原型对象,一些公共的属性或者方法放到这个原型对象身上,但是为啥实例对象可以访问原型对象里面的属性和方法呢?

这里就要说到我们的对象原型了。

3.7.2 对象原型

对象都会有一个属性 ___proto__ 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto__ 原型的存在。

三者之间的关系

function Star(){
    console.log(123)
}
const ldh = new Star();//实例化对象
console.log(ldh.__proto__ === Star.prototype)

可见__proto__指向了原型对象

注意:

  • __proto__ 是JavaScript非标准属性,所以打印的时候显示出来的都是[[prototype]]
  • [[prototype]]__proto__意义相同
  • 用来表明当前实例对象指向哪个原型对象prototype
  • __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数

总结:

3.7.3 原型继承

继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性

 // 继续抽取   公共的部分放到原型上
// const Person1 = {
//   eyes: 2,
//   head: 1
// }
// const Person2 = {
//   eyes: 2,
//   head: 1
// }
// 构造函数  new 出来的对象 结构一样,但是对象不一样
function Person() {
  this.eyes = 2
  this.head = 1
}
// console.log(new Person)
// 女人  构造函数   继承  想要 继承 Person
function Woman() {

}
// Woman 通过原型来继承 Person
// 父构造函数(父类)   子构造函数(子类)
// 子类的原型 =  new 父类(重要)  
Woman.prototype = new Person()   //这里就是继承 {eyes: 2, head: 1} 
// 指回原来的构造函数(重要)
Woman.prototype.constructor = Woman

// 给女人添加一个方法  生孩子
Woman.prototype.baby = function () {
  console.log('宝贝')
}
const red = new Woman()
console.log(red)
// console.log(Woman.prototype)
// 男人 构造函数  继承  想要 继承 Person
function Man() {

}
// 通过 原型继承 Person
Man.prototype = new Person()
Man.prototype.constructor = Man
const pink = new Man()
console.log(pink)

3.7.4 原型链

基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链。

原型链

只要是对象,就有__proto__ ;只要是原型对象就有constructor

原型链其实就是一个查找规则:

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
  3. 如果还没有就查找原型对象的原型(Object的原型对象
  4. 依此类推一直找到 Object 为止(null
  5. __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
  6. 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
// function Objetc() {}
console.log(Object.prototype)
console.log(Object.prototype.__proto__)

function Person() {

}
const ldh = new Person()
// console.log(ldh.__proto__ === Person.prototype)
// console.log(Person.prototype.__proto__ === Object.prototype)
console.log(ldh instanceof Person)
console.log(ldh instanceof Object)
console.log(ldh instanceof Array)
console.log([1, 2, 3] instanceof Array)
console.log(Array instanceof Object)

3.8 深浅拷贝

3.8.1 浅拷贝

以前我们拷贝对象是选择直接赋值的形式:

const obj = {
    uname:"念心卓",
    age:18
}
const newObj = obj;

直接赋值的形式就会导致一个问题,就是你新对象的修改会影响原对象。

首先浅拷贝和深拷贝只针对引用类型

浅拷贝:拷贝的是地址

常见方法:

  1. 拷贝对象:Object.assgin() 或者 展开运算符 {…obj} 拷贝对象

    const obj = {
        uname:"念心卓",
        age:18
    }
    //浅拷贝对象 或者const newObj = {...obj};
    const newObj = {};
    Object.assign(newObj,obj);
    newObj.age = 20;
    console.log(newObj);
    console.log(obj);
    

    执行结果

  2. 拷贝数组:Array.prototype.concat() 或者 […arr]

如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)

例如:

const obj = {
    uname:"念心卓",
    age:18,
    family:{
        baby:'小baby'
    }
}
//浅拷贝对象
const newObj = {};
Object.assign(newObj,obj);
newObj.age = 20;
newObj.family.baby = '老baby';
console.log(newObj);
console.log(obj);

可见都被改了

所以:

总结

3.8.2 深拷贝

首先浅拷贝和深拷贝只针对引用类型

深拷贝:拷贝的是对象,不是地址

常见方法:

  1. 通过递归实现深拷贝

    函数递归:如果一个函数在内部可以调用其本身,那么这个函数就是递归函数

    • 简单理解:函数内部自己调用自己, 这个函数就是递归函数
    • 递归函数的作用和循环效果类似
    • 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
    const obj = {
      uname: 'pink',
      age: 18,
      hobby: ['乒乓球', '足球'],
      family: {
        baby: '小pink'
      }
    }
    const o = {}
    // 拷贝函数
    function deepCopy(newObj, oldObj) {
      debugger
      for (let k in oldObj) {
        // 处理数组的问题  一定先写数组 在写 对象 不能颠倒 (小范围到大范围)
        if (oldObj[k] instanceof Array) {
          newObj[k] = []
          //  newObj[k] 接收 []  hobby
          //  oldObj[k]   ['乒乓球', '足球']
          deepCopy(newObj[k], oldObj[k])
        } else if (oldObj[k] instanceof Object) {
          newObj[k] = {}
          deepCopy(newObj[k], oldObj[k])
        }
        else {
          //  k  属性名 uname age    oldObj[k]  属性值  18
          // newObj[k]  === o.uname  给新对象添加属性
          newObj[k] = oldObj[k]
        }
      }
    }
    deepCopy(o, obj) // 函数调用  两个参数 o 新对象  obj 旧对象
    console.log(o)
    o.age = 20
    o.hobby[0] = '篮球'
    o.family.baby = '老pink'
    console.log(obj)
    console.log([1, 23] instanceof Object) //true Object是引用类型的父类型
    
  2. lodash/cloneDeep - 第三方库

    <body>
      <!-- 先引用 -->
      <script src="./lodash.min.js"></script>
      <script>
        const obj = {
          uname: 'pink',
          age: 18,
          hobby: ['乒乓球', '足球'],
          family: {
            baby: '小pink'
          }
        }
        const o = _.cloneDeep(obj)
        console.log(o)
        o.family.baby = '老pink'
        console.log(obj)
      </script>
    </body>
    
  3. 通过JSON.stringify()实现

    const obj = {
        uname: '念心卓',
        age: 18,
        hobby: ['乒乓球', '足球'],
        family: {
            baby: '小baby'
        }
    }
    // 把对象转换为 JSON 字符串
    // console.log(JSON.stringify(obj))
    const o = JSON.parse(JSON.stringify(obj)) //先转为JSON字符串,再转为复杂对象
    console.log(o)
    o.family.baby = '123'
    console.log(obj)
    

    结果

3.9 异常处理

3.9.1 throw

异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行

总结:

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 throw 使用,能够设置更详细的错误信息
function counter(x, y) {
    if(!x || !y) {
      // throw '参数不能为空!';
      throw new Error('参数不能为空!') //Error 对象配合 throw 使用
    }
    return x + y
}
counter()

3.9.2 try … catch

function foo() {
  try {
    // 查找 DOM 节点
    const p = document.querySelector('.p')
    p.style.color = 'red'
  } catch (error) {
    // try 代码段中执行有错误时,会执行 catch 代码段
    // 查看错误信息
    console.log(error.message)
    // 终止代码继续执行 (重要) 或者不适用return 使用之前的 throw new Error
    return
  }
  finally { //无论你的代码对不对,这段代码都会执行
      alert('执行')
  }
  console.log('如果出现错误,我的语句不会执行')
}
foo()

总结:

  1. try...catch 用于捕获错误信息
  2. 将预估可能发生错误的代码写在 try 代码段中
  3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息,catch里面可以接收一个参数(error)

3.9.3 debugger

相当于断点调试,你只要再你的代码中写了debugger,启动的时候就会停止再这等你调试

相当于Java中IDEA的debug

例如:

function f() {
   debugger //设置断点
   let i = 1;
    console.log(i++);
}
f();

启动的时候程序就会卡在这等你调试

3.10 处理this

this 是 JavaScript 最具“魅惑”的知识点,不同的应用场合 this 的取值可能会有意想不到的结果,在此我们对以往学习过的关于【 this 默认的取值】情况进行归纳和总结。

3.10.1 普通函数

普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】,如下代码所示:

// 普通函数
function sayHi() {
    console.log(this)  
}
// 函数表达式
const sayHello = function () {
    console.log(this)
}
// 函数的调用方式决定了 this 的值
sayHi() // window
window.sayHi()
    

// 普通对象
const user = {
    name: '小明',
    walk: function () {
          console.log(this)
    }
}
// 动态为 user 添加方法
user.sayHi = sayHi
uesr.sayHello = sayHello
// 函数调用方式,决定了 this 的值
user.sayHi()
user.sayHello()

在JavaScript中,还有一种模式称为严格模式,在代码的第一行中使用use strict

'use strict' //开启严格模式
function f() {
    console.log(this) //undefined
}
f()

严格模式:即所有的代码必须按照正常模式来写,包括变量的先声明后使用,所以这就是为什么上面的this在严格模式下输出为undefined的原因。

注: 普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined

3.10.2 箭头函数

箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this箭头函数中访问的 this 不过是箭头函数所在作用域的 this 变量。

  1. 箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的
  2. **箭头函数中的this引用的就是最近作用域中的this **
  3. 向外层作用域中,一层一层查找this,直到有this的定义
console.log(this) // 此处为 window
// 箭头函数
const sayHi = function() {
    console.log(this) // 该箭头函数中的 this 为函数声明环境中 this 一致
}
// 普通对象
const user = {
    name: '小明',
    // 该箭头函数中的 this 为函数声明环境中 this 一致
    walk: () => {
      console.log(this) //箭头函数的this绑定的是最近一级的this,由于user中的没有this,所以继续往外层找,找到了这个window。
    },

    sleep: function () {
      let str = 'hello'
      console.log(this)
      let fn = () => {
        console.log(str)
        console.log(this) // 该箭头函数中的 this 与 sleep 中的 this 一致
      }
      // 调用箭头函数
      fn();
    }
}

// 动态添加方法
user.sayHi = sayHi

// 函数调用
user.sayHi()
user.sleep()
user.walk()

在开发中【使用箭头函数前需要考虑函数中 this 的值】事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数不推荐使用箭头函数,如下代码所示:

// DOM 节点
const btn = document.querySelector('.btn')
// 箭头函数 此时 this 指向了 window
btn.addEventListener('click', () => {
    console.log(this)
})
// 普通函数 此时 this 指向了 DOM 对象
btn.addEventListener('click', function () {
    console.log(this)
})

同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:

function Person() {
}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
    console.log('人都要走路...')
    console.log(this); // window
}
const p1 = new Person()
p1.walk()

总结

3.10.3 改变this指向

以上归纳了普通函数和箭头函数中关于 this 默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向:

  1. call

    使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:

    <script>
      // 普通函数
      function sayHi() {
        console.log(this);
      }
    
      let user = {
        name: '小明',
        age: 18
      }
    
      let student = {
        name: '小红',
        age: 16
      }
    
      // 调用函数并指定 this 的值
      sayHi.call(user); // 函数里的this 值为 user
      sayHi.call(student); // 函数里的this 值为 student
    
      // 求和函数
      function counter(x, y) {
        return x + y;
      }
    
      // 调用 counter 函数,并传入参数
      let result = counter.call(null, 5, 10); //第一个参数为指定this指向,其他参数为正常实参
      console.log(result);
    </script>
    

    总结:

    1. call 方法能够在调用函数的同时指定 this 的值
    2. 使用 call 方法调用函数时,第1个参数为 this 指定的值
    3. call 方法的其余参数会依次自动传入函数做为函数的参数
    4. call的返回值就是函数的返回值,因为他就是调用函数
  2. apply

    使用 apply 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:

    <script>
      // 普通函数
      function sayHi() {
        console.log(this)
      }
    
      let user = {
        name: '小明',
        age: 18
      }
    
      let student = {
        name: '小红',
        age: 16
      }
    
      // 调用函数并指定 this 的值
      sayHi.apply(user) // this 值为 user
      sayHi.apply(student) // this 值为 student
    
      // 求和函数
      function counter(x, y) {
        return x + y
      }
      // 调用 counter 函数,并传入参数
      let result = counter.apply(null, [5, 10]) //其他正常的实参必须在数组中
      console.log(result)
    </script>
    

    总结:

    1. apply 方法能够在调用函数的同时指定 this 的值
    2. 使用 apply 方法调用函数时,第1个参数为 this 指定的值
    3. apply 方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
    4. call的返回值就是函数的返回值,因为他就是调用函数

    与call很像,但是也有一些不一样,call后面传递的实参就是依次写如即可,而apply后面的实参只有一个,必须传递一个数组值。

  3. bind

    bind 方法并不会调用函数,而是创建一个指定了 this 值的新函数,使用方法如下代码所示:

    <script>
      // 普通函数
      function sayHi() {
        console.log(this)
      }
      let user = {
        name: '小明',
        age: 18
      }
      // 调用 bind 指定 this 的值
      let sayHello = sayHi.bind(user);
      // 调用使用 bind 创建的新函数
      sayHello()
    </script>
    

    注:bind 方法创建新的函数,与原函数的唯一的变化是改变了 this 的值。

    返回由指定的this值和初始化参数改造的原函数拷贝(新函数),因此当我们只是想改变this指向,并且不想调用这个函数的时候,可以使用bind,比如改变定时器内部的 this指向。

总结

3.11 防抖节流

  1. 防抖(debounce)
    所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间
  2. 节流(throttle)
    所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数

文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录