javascript核心进阶-ES6语法、API、js高级等基础知识

75 阅读1小时+

黑马程序员前端JavaScript入门到精通全套视频教程,javascript核心进阶ES6语法、API、js高级等基础知识和实战教程-----js进阶部分笔记

image.png

image.png

作用域&解构&箭头函数

学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。

  • 理解作用域对程序执行的影响
  • 能够分析程序执行的作用域范围
  • 理解闭包本质,利用闭包创建隔离作用域
  • 了解什么变量提升及函数提升
  • 掌握箭头函数、解析剩余参数等简洁语法

1. 作用域

了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。

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

局部作用域

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

函数作用域

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

<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. 函数执行完毕后,函数内部的变量实际被清空了
块作用域

在 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 声明成常量。

总结
  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 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。

作用域链

在解释什么是作用域链前先来看一段代码:

<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. 子作用域能够访问父作用域,父级作用域无法访问子级作用域

其实作用域链就像链子一样,是按着从小到大的规则查找的

JS 垃圾回收机制

目标:了解JS垃圾回收机制的执行过程

学习目的: 为了闭包做铺垫

学习路径:

  1. 什么是垃圾回收机制

  2. 内存的声明周期

  3. 垃圾回收的算法说明

什么是垃圾回收机制?

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

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

正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题

但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况

内存的生命周期

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

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存

  2. 内存使用:即读写内存,也就是使用变量、函数等

  3. 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存

  4. 说明:

    全局变量一般不会回收(关闭页面回收);

    一般情况下局部变量的值, 不用了, 会被自动回收掉

内存泄漏

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

简单来说,不再用到的内存,没有及时释放,就叫做内存泄漏

总结
  1. 什么是垃圾回收机制?

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

  2. 什么是内存泄漏?

    不再用到的内存,没有及时释放,就叫做内存泄漏

  3. 内存的生命周期是什么样的?

    内存分配、内存使用、内存回收

    全局变量一般不会回收; 一般情况下局部变量的值, 不用了, 会被自动回收掉

拓展-JS垃圾回收机制-算法说明

堆栈空间分配区别:

  1. 栈(操作系统): 由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。

  2. 堆(操作系统): 一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。

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

引用计数

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

算法:

  1. 跟踪记录被引用的次数

  2. 如果被引用了一次,那么就记录次数1,多次引用会累加 ++

  3. 如果减少一个引用就减1 --

  4. 如果引用次数是0 ,则释放内存

image.png

由上面可以看出,引用计数算法是个简单有效的算法。

引用计数存在问题

但它却存在一个致命的问题:嵌套引用(循环引用)

如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。

image.png 因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露

标记清除法

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

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

核心:

  1. 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。

  2. 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。

  3. 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

image.png

总结:标记清除法核心思路是什么?

从根部扫描对象,能查找到的就是使用的,查找不到的就要回收

闭包

目标: 能说出什么是闭包,闭包的作用以及注意事项

闭包概念

概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域

简单理解:闭包 =  内层函数 + 外层函数的变量

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

image.png image.png

<script>
// 1. 闭包 : 内层函数 + 外层函数变量
 function outer() {
   const a = 1 // 外层函数变量
   function f() { // 内层函数使用到了外层函数变量
     console.log(a)
   }
   f()
 }
 outer()
</script>
闭包作用

闭包作用:封闭数据,提供操作,外部也可以访问函数内部的变量

function outer() {
  let a = 10
  // function fn () {
  //   console.log(a)
  // }
  // return fn
  return function () {
    console.log(a)
  }
}
const fun = outer()
fun() // 调用函数

//外面要使用这个 10
// outer()  ===  fn  ===  function fn() {}
// const fun = function fn() { }
闭包应用

闭包应用:实现数据的私有

比如,我们要做个统计函数调用次数,函数调用一次,就++

// 普通形式 统计函数调用的次数
let i = 0
function fn() {
  i++
  console.log(`函数被调用了${i}次`)
}
//  因为 i 是全局变量,容易被修改

// 闭包形式 统计函数调用的次数
function count() {
  let i = 0
  function fn() {
    i++
    console.log(`函数被调用了${i}次`)
  }
  return fn
}
const fun = count()
//i不会被回收,除非关闭页面,这就是内存泄漏
总结

1.怎么理解闭包?

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

2.闭包的作用?

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

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

  • 内存泄漏

变量提升

目标:了解什么是变量提升

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

注意

  1. 把所有var声明的变量提升到 当前作用域的最前面
  2. 只提升声明,不提升赋值
console.log(num + '件') // undefined件
var num = 10
console.log(num) //10

//上述代码相当于
// var num
// console.log(num + '件')
// num = 10
// console.log(num)
function fn() {
  console.log(num) //undefined
  var num = 10
}
fn()

//上述fn函数代码相当于
// function fn() {
//   var num
//   console.log(num);
//   num = 10;
// }

总结:

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

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

总结
  1. 用哪个关键字声明变量会有变量提升?

    var

  2. 变量提升是什么流程?

    先把var 变量提升到当前作用域于最前面

    只提升变量声明,不提升变量赋值

    然后依次执行代码

    我们不建议使用var声明变量

2. 函数进阶

知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。

函数提升

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

会把所有函数声明提升到当前作用域的最前面;只提升函数声明,不提升函数调用

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

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

总结:

  1. 函数提升能够使函数的声明调用更灵活
  2. 函数表达式必须先声明和赋值,后调用,否则报错,不存在提升的现象
  3. 函数提升出现在相同作用域当中

函数参数

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

默认值
<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 的作用是动态获取函数的实参
  3. 可以通过for循环依次得到传递过来的实参
剩余参数

剩余参数允许我们将一个不定数量的参数表示为一个数组

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

那和arguments 有什么不同吗?

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

开发中,还是提倡多使用 剩余参数。

注意:箭头函数里面没有arguments

展开运算符

能够使用展开运算符并说出常用的使用场景

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

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

说明:

  1. 不会修改原数组

典型运用场景: 求数组最大值(最小值)、合并数组等

展开运算符 or 剩余参数

剩余参数:函数参数使用,得到真数组

展开运算符:数组中使用,数组展开

总结
  1. 当不确定传递多少个实参的时候,我们怎么办?

    arguments 动态参数

  2. arguments是什么?

    伪数组

    它只存在函数中

  3. 剩余参数主要的使用场景是?

    用于获取多余的实参(不确定有多少个实参传递过来)

  4. 剩余参数和动态参数区别是什么?开发中提倡使用哪一个?

    动态参数是伪数组

    剩余参数是真数组

    开发中使用剩余参数想必也是极好的

  5. 展开运算符主要的作用是?

    可以把数组展开,可以利用求数组最大值以及合并数组等操作

  6. 展开运算符和剩余参数有什么区别?

    展开运算符主要是 数组展开

    剩余参数 在函数内部使用

箭头函数

能够熟悉箭头函数不同写法

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

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

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

基本写法
<body>
  <script>
    // 匿名函数
    // const fn = function () {
    //   console.log(123)
    // }
    
    // 1. 箭头函数 基本语法
    // const fn = () => {
    //   console.log(123)
    // }
    // fn()
    
    //1.1. 带参数
    // const fn = (x) => {
    //   console.log(x)
    // }
    // fn(1)
    
    // 2. 只有一个形参的时候,可以省略小括号
    // const fn = x => {
    //   console.log(x)
    // }
    // fn(1)
    
    // // 3. 只有一行代码的时候,我们可以省略大括号
    // const fn = x => console.log(x)
    // fn(1)
    
    // 4. 只有一行代码的时候,并且有返回值的时候,可以省略return
    // const fn = x => x + x
    // console.log(fn(1))
    
    // 5. 箭头函数可以直接返回一个对象
    // const fn = (uname) => ({ uname: uname })
    // console.log(fn('刘德华'))
  </script>
</body>

总结:

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

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

<body>
  <script>
    // 1. 利用箭头函数来求和
    const getSum = (...arr) => {
      let sum = 0
      for (let i = 0; i < arr.length; i++) {
        sum += arr[i]
      }
      return sum
    }
    const result = getSum(2, 3, 4)
    console.log(result) // 9
  </script>
箭头函数 this

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

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

 <script>
    // 以前this的指向:  谁调用的这个函数,this 就指向谁
    
    // console.log(this)  // window
    
    // // 普通函数
    // function fn() {
    //   console.log(this)  // window
    // }
    // window.fn()
    
    // // 对象方法里面的this
    // const obj = {
    //   name: 'andy',
    //   sayHi: function () {
    //     console.log(this)  // obj
    //   }
    // }
    // obj.sayHi()

    // 2. 箭头函数的this  是上一层作用域的this 指向
    // const fn = () => {
    //   console.log(this)  // window
    // }
    // fn()
    
    // 对象方法箭头函数 this
    // const obj = {
    //   uname: 'pink老师',
    //   sayHi: () => {
    //     console.log(this)  // this 指向谁? window
    //   }
    // }
    // obj.sayHi()

    const obj = {
      uname: 'pink老师',
      sayHi: function () {
        console.log(this)  // obj
        let i = 10
        const count = () => {
          console.log(this)  // obj 
        }
        count()
      }
    }
    obj.sayHi()

  </script>

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

总结
  1. 箭头函数里面有arguments动态参数吗?可以使用什么参数?

    没有arguments动态参数

    可以使用剩余参数

  2. 箭头函数里面有this吗?

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

  3. DOM事件回调函数推荐使用箭头函数吗?

    不太推荐,特别是需要用到this的时候

    事件回调函数使用箭头函数时,this 为全局的 window

3. 解构赋值

知道解构的语法及分类,使用解构简洁语法快速为变量赋值。

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

数组解构

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

基本语法:

  1. 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
  2. 变量的顺序对应数组单元值的位置依次进行赋值操作

如下代码所示:

<script>
    const [max, min, avg] = [100, 60, 80]
    //相当于
    // const max = arr[0]
    // const min = arr[1]
    // const avg = arr[2]


  //典型应用,交换两个变量的值
  // let a = 1
  // let b = 2;
  // [b, a] = [a, b]
  // console.log(a, b)
  
  注意上下两种加逗号位置的情况
  
  let a = 1
  let b = 2
  ;[b, a] = [a, b]
  console.log(a, b)
</script>
// 1. 变量多, 单元值少 , undefined
// const [a, b, c, d] = [1, 2, 3]
// console.log(a) // 1
// console.log(b) // 2
// console.log(c) // 3
// console.log(d) // undefined

// 2. 变量少, 单元值多
// const [a, b] = [1, 2, 3]
// console.log(a) // 1
// console.log(b) // 2

// 3.  剩余参数 变量少, 单元值多
// const [a, b, ...c] = [1, 2, 3, 4]
// console.log(a) // 1
// console.log(b) // 2
// console.log(c) // [3, 4]  真数组

// 4.  防止 undefined 传递
// const [a = 0, b = 0] = [1, 2]
// console.log(a) // 1
// console.log(b) // 2
// const [a = 0, b = 0] = []
// console.log(a) // 0
// console.log(b) // 0

// 5.  按需导入赋值
// const [a, b, , d] = [1, 2, 3, 4]
// console.log(a) // 1
// console.log(b) // 2
// console.log(d) // 4

//多维数组无解构
// const arr = [1, 2, [3, 4]]
// console.log(arr[0])  // 1
// console.log(arr[1])  // 2
// console.log(arr[2])  // [3,4]
// console.log(arr[2][0])  // 3

// 多维数组解构
// const arr = [1, 2, [3, 4]]
// const [a, b, [c]] = [1, 2, [3, 4]]
// console.log(a) // 1
// console.log(b) // 2
// console.log(c) // 3

总结:

  1. 变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
  2. 变量的数量小于单元值数量时,可以通过 剩余参数... 获取剩余单元值,但只能置于最末位
  3. 允许初始化变量的默认值,且只有单元值为 undefined 时,默认值才会生效

对象解构

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

基本语法:

  1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  2. 对象属性的值将被赋值给与属性名相同的变量
  3. 注意解构的变量名不要和外面的变量名冲突否则报错
  4. 对象中找不到与变量名一致的属性时变量值为 undefined

如下代码所示:

<script>
  // 普通对象
  const user = {
    name: '小明',
    age: 18
  };
  // 批量声明变量 name age
  // 同时将数组单元值 小明  18 依次赋值给变量 name  age
  // const {name, age} = user
  // console.log(name) // 小明
  // console.log(age) // 18
  
  //对象解构的变量名,可以重新改名
  //旧变量名: 新变量名
  const {name: username, age, sex} = uesr
  console.log(name) // ''
  console.log(username) // 小明
  console.log(age) // 18
  console.log(sex) // undefined
</script>

可以从一个对象中提取变量并同时修改新的变量名

允许初始化变量的默认值,属性不存在或单元值为 undefined 时默认值才会生效

注:支持多维解构赋值

<body>
  <script>
    // 1. 这是后台传递过来的数据
    const msg = {
      "code": 200,
      "msg": "获取新闻列表成功",
      "data": [
        {
          "id": 1,
          "title": "5G商用自己,三大运用商收入下降",
          "count": 58
        },
        {
          "id": 2,
          "title": "国际媒体头条速览",
          "count": 56
        },
        {
          "id": 3,
          "title": "乌克兰和俄罗斯持续冲突",
          "count": 1669
        },

      ]
    }
    // 需求1: 请将以上msg对象  采用对象解构的方式 只选出  data 方面后面使用渲染页面
    // const { data } = msg
    // console.log(data)
    // 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数
    // const { data } = msg
    // msg 虽然很多属性,但是我们利用解构只要 data值
    // function render({ data }) {
      // const { data } = arr
      // 我们只要 data 数据
      // 内部处理
      // console.log(data)
    // }
    // render(msg)

    // 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData
    function render({ data: myData }) {
      // 要求将 获取过来的 data数据 更名为 myData
      // 内部处理
      console.log(myData)
    }
    render(msg)
  </script>

总结

  1. 数组解构赋值的作用是什么?

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

  2. Js 前面有两哪种情况需要加分号的?

    立即执行函数

    数组解构(数组开头的语句)

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

    undefined

  4. 变量的数量小于单元值数量时,可以通过什么剩余获取所有的值?

    剩余参数... 获取剩余单元值,但只能置于最末位

综合案例

forEach遍历数组

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

主要使用场景: 遍历数组的每个元素

语法:被遍历的数组.forEach(function(当前数组元素, 当前数组元素索引号)) { // 函数体 }

注意:

1.forEach 主要是遍历数组

2.参数当前数组元素是必须要写的, 索引号可选。

<body>
  <script>
    // forEach 就是遍历  加强版的for循环  适合于遍历数组对象
    const arr = ['red', 'green', 'pink']
    const result = arr.forEach(function (item, index) {
      console.log(item)  // 数组元素 red  green pink
      console.log(index) // 索引号
    })
    // console.log(result)
  </script>
</body>

filter筛选数组

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

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

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

返回值:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组

参数:currentValue 必须写, index 可选

因为返回新数组,所以不会影响原数组

<body>
  <script>
    const arr = [10, 20, 30]
    // const newArr = arr.filter(function (item, index) {
    //   // console.log(item)
    //   // console.log(index)
    //   return item >= 20
    // })
    // 返回的符合条件的新数组

    const newArr = arr.filter(item => item >= 20)
    console.log(newArr)
  </script>
</body>

构造函数&数据常用函数

了解面向对象编程的基础概念及构造函数的作用,体会 JavaScript 一切皆对象的语言特征,掌握常见的对象属性和方法的使用。

  • 掌握基于构造函数创建对象,理解实例化过程
  • 掌握对象数组字符数字等类型的常见属性和方法,便捷完成功能
  • 了解面向对象编程中的一般概念
  • 能够基于构造函数创建对象
  • 理解 JavaScript 中一切皆对象的语言特征
  • 理解引用对象类型值存储的的特征
  • 掌握包装类型对象常见方法的使用

深入对象

创建对象三种方式

  1. 利用对象字面量创建对象 const obj = {name: 'xx'}

  2. 利用 new Object 创建对象 const obj = new Object({name: 'xx'})

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

构造函数

构造函数是专门用于创建对象的函数,如果一个函数使用 new 关键字调用,那么这个函数就是构造函数。

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

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

构造函数在技术上是常规函数。

不过有两个约定:

  1. 它们的命名以大写字母开头。

  2. 它们只能由 "new" 操作符来执行。

<script>
  // 定义函数
  function foo() {
    console.log('通过 new 也能调用函数...');
  }
  // 调用函数
  new foo;
</script>

构造函数语法:大写字母开头的函数

创建构造函数:

// 创建构造函数
function Pig(uname) {
  this.uname = uname
}
//new 关键字调用函数
const p = new Pig('佩奇')
console.log(p)

总结:

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

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

  3. 构造函数内部无需写return,返回值即为新创建的对象

  4. 构造函数内部的 return 返回的值无效,所以不要写return

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

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

实例化执行过程

说明:

  1. 创建新的空对象

  2. 构造函数this指向新对象

  3. 执行构造函数代码,修改this,添加新的属性

  4. 返回新对象

实例成员

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

<script>
  // 构造函数
  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() // 调用实例方法
</script>

总结:

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

静态成员

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

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

总结:

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

总结

  1. 构造函数的作用是什么?怎么写呢?

    构造函数是来快速创建多个类似的对象

    大写字母开头的函数

  2. new 关键字调用函数的行为被称为?

    实例化

  3. 构造函数内部需要写return吗,返回值是什么?

    不需要

    构造函数自动返回创建的新的对象

  4. 什么是实例成员?

    实例对象的属性和方法即为实例成员

  5. 实例成员(属性和方法)写在谁身上? 实例对象的属性和方法即为实例成员 实例对象相互独立,实例成员当前实例对象使用

  6. 什么是静态成员?

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

  7. 静态成员(属性和方法)写在谁身上?

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

    静态成员只能构造函数访问

内置构造函数

掌握各引用类型和包装类型对象属性和方法的使用。

在 JavaScript 中最主要的数据类型有 6 种, 分别是字符串、数值、布尔、undefined、null 和 对象,常见的对象类型数据包括数组和普通对象。其中字符串、数值、布尔、undefined、null 也被称为简单类型或基础类型,对象也被称为引用类型。

其实字符串、数值、布尔等基本类型也都有专门的构造函数,这些我们称为包装类型。(js底层完成,把简单数据类型包装为了引用数据类型)

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

内置构造函数分为:

引用类型

  • Object,Array,RegExp,Date 等

包装类型

  • String,Number,Boolean 等
<script>
  // 实例化
	let date = new Date();
  
  // date 即为实例对象
  console.log(date);
</script>

甚至字符串、数值、布尔、数组、普通对象也都有专门的构造函数,用于创建对应类型的数据。

Object

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

推荐使用字面量方式声明对象,而不是 Object 构造函数

<script>
  // 通过构造函数创建普通对象
  const user = new Object({name: '小明', age: 15})

  // 这种方式声明的变量称为【字面量】
  let student = {name: '杜子腾', age: 21}
  
  // 对象语法简写
  let name = '小红';
  let people = {
    // 相当于 name: name
    name,
    // 相当于 walk: function () {}
    walk () {
      console.log('人都要走路...');
    }
  }

  console.log(student.constructor);
  console.log(user.constructor);
  console.log(student instanceof Object);
</script>

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

  • Object.keys
    • 作用:Object.keys 静态方法获取对象中所有属性(键)
    • 语法:Object.keys(obj)
    • 注意: 返回的是一个数组
  • Object.values
    • 作用:Object.values 静态方法获取对象中所有属性值
    • 语法:Object.keys(obj)
    • 注意: 返回的是一个数组
  • Object. assign
    • 作用:Object. assign 静态方法常用于对象拷贝
    • 语法:Object.keys(objNew, objOld)
    • 使用:经常使用的场景给对象添加属性 Object. assign(o, {gender: '女'})

总结:

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

Array

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

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

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

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

image.png

总结:

  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 查找元素的索引值

  14. 静态方法 Array.from() 伪数组转换为真数组

  15. 实例方法 reduce 返回函数累计处理的结果,经常用于求和等

    • 语法

    • arr.reduce(function(){}, 起始值)

      arr.reduce(function(累计值(上一次值), 当前元素){}, 起始值)

    • 累计值参数:

      1. 如果有起始值,则以起始值为准开始累计,累计值 = 起始值

      2. 如果没有起始值,则累计值以数组的第一个数组元素作为起始值开始累计

      3. 后面每次遍历就会用后面的数组元素 累计到 累计值 里面 (类似求和里面的 sum ) image.png

//注意:数组对象里的数据,初始值必须有值,否则第一次循环prev的值为对象会报错
const arr = [{
  name: '张三',
  salary: 10000
}, {
  name: '李四',
  salary: 10000
}, {
  name: '王五',
  salary: 20000
},
]
const money = arr.reduce(function (prev, item) {
  return prev + item.salary * 0.3
}, 0)

包装类型

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

<script>
  // 字符串类型
  const str = 'hello world!'
 // 统计字符的长度(字符数量)
  console.log(str.length)
  
  // 数值类型
  const price = 12.345
  // 保留两位小数
  price.toFixed(2) // 12.34
</script>

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

String

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

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

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

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

总结:

  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 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。

Number

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

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

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

</script>

总结:

  1. 推荐使用字面量方式声明数值,而不是 Number 构造函数
  2. 实例方法 toFixed 用于设置保留小数位的长度 str.toFixed()

总结

  1. 什么是静态方法?

    只能给构造函数使用的方法 比如 Object.keys()

  2. Object.keys()方法的作用是什么 ?

    获取对象中所有属性(键)

  3. Object.values()方法的作用是什么 ?

    获取对象中所有属性值(值)

深入面向对象

了解构造函数原型对象的语法特征,掌握 JavaScript 中面向对象编程的实现方式,基于面向对象编程思想实现 DOM 操作的封装。

  • 了解面向对象编程的一般特征
  • 掌握基于构造函数原型对象的逻辑封装
  • 掌握基于原型对象实现的继承
  • 理解什么原型链及其作用
  • 能够处理程序异常提升程序执行的健壮性

编程思想

学习 JavaScript 中基于原型的面向对象编程序的语法实现,理解面向对象编程的特征。

面向过程

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

举个栗子:蛋炒饭

image.png

面向过程,就是按照我们分析好了的步骤,按照步骤解决问题。

面向对象

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

image.png

面向对象是以对象功能来划分问题,而不是步骤。

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

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

面向对象的特性:

  • 封装性

  • 继承性

  • 多态性

面向过程和面向对象的对比

面向过程编程

  • 优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。

  • 缺点:没有面向对象易维护、易复用、易扩展

面向对象编程

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护

  • 缺点:性能比面向过程低

生活离不开蛋炒饭,也离不开盖浇饭,选择不同而已,只不过前端不同于其他语言,面向过程更多

构造函数

对比以下通过面向对象的构造函数实现的封装:

<script>
  function Person() {
    this.name = '佚名'
    // 设置名字
    this.setName = function (name) {
      this.name = name
    }
    // 读取名字
    this.getName = () => {
      console.log(this.name)
    }
  }

  // 实例对像,获得了构造函数中封装的所有逻辑
  let p1 = new Person()
  p1.setName('小明')
  console.log(p1.name)

  // 实例对象
  let p2 = new Person()
  console.log(p2.name)
</script>

封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。

同样的将变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的

总结:

  1. 构造函数体现了面向对象的封装特性
  2. 构造函数实例创建的对象彼此独立、互不影响

面向对象编程的特性:比如封装性、继承性等,可以借助于构造函数来实现

前面我们学过的构造函数方法很好用,但是 存在浪费内存的问题

我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎样做呢?

原型

什么是原型

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

  • JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
  • 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
  • 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
  • 构造函数和原型对象中的this 都指向 实例化的对象

image.png

<script>
  function Person() {
    
  }

  // 每个函数都有 prototype 属性
  console.log(Person.prototype)
</script>

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

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

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

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

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

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

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

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

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

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

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

//原型对象中的this 都指向 实例化的对象 arr
const arr = [1, 2, 3];
Array.prototype.max = function () {
return Math.max(...this); //this相当于arr
};
console.log(arr.max());

总结

  1. 原型是什么 ?

    一个对象,我们也称为 prototype 为原型对象

  2. 原型的作用是什么 ?

    共享方法

    可以把那些不变的方法,直接定义在 prototype 对象上

  3. 构造函数和原型里面的this指向谁 ?

    实例化的对象

  4. 公共的属性写到构造函数里面,公共的方法写到原型对象身上

constructor 属性

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

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

image.png

function Star() {}
const ldh = new Star()
console.log(Star.prototype)
console.log(Star.prototype.constructor === Star) //true

使用场景:

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

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

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

// constructor  单词 构造函数

// Star.prototype.sing = function () {
//   console.log('唱歌')
// }
// Star.prototype.dance = function () {
//   console.log('跳舞')
// }
function Star() {
}
// console.log(Star.prototype)
Star.prototype = {
  // 从新指回创造这个原型对象的 构造函数
  constructor: Star,
  sing: function () {
    console.log('唱歌')
  },
  dance: function () {
    console.log('跳舞')
  },
}
console.log(Star.prototype)

对象原型

思考

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

image.png

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

image.png

function Star() {}
const ldh = new Star()
console.log(ldh.__proto__ === Star.prototype) // true
console.log(Star.prototype.constructor === Star) // true
console.log(ldh.__proto__.constructor === Star) // true
console.log(ldh.__proto__.constructor === Star.prototype.constructor) // true

注意:

  • __proto__ 是JS非标准属性
  • [[prototype]]和__proto__意义相同(因为非标准,个别浏览器显示可能不太一样,比如谷歌显示的是[[prototype]]只是用来展示,只读的,如果要打印,用的还是ldh.__proto__)
  • 用来表明当前实例对象指向哪个原型对象prototype
  • __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数

总结

  1. prototype是什么?哪里来的?

    原型(原型对象)

    构造函数都自动有原型

  2. constructor属性在哪里?作用干啥的?

    prototype原型和对象原型__proto__里面都有

    都指向创建实例对象/原型的构造函数

  3. __proto__属性在哪里?指向谁?

    在实例对象里面

    指向原型 prototype

原型继承

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

龙生龙、凤生凤、老鼠的儿子会打洞描述的正是继承的含义。

  1. 封装-抽取公共部分

    把男人和女人公共的部分抽取出来放到人类里面

  2. 继承-让男人和女人都能继承人类的一些属性和方法

    把男人女人公共的属性和方法抽取出来 People

    然后赋值给Man的原型对象,可以共享这些属性和方法

    注意让constructor指回Man这个构造函数

  3. 问题:

    如果我们给男人添加了一个吸烟的方法,发现女人自动也添加这个方法

    --原因

    男人和女人都同时使用了同一个对象,根据引用类型的特点,他们指向同一个对象,修改一个就会都影响

    //继续抽取   公共的部分放到原型上
    const Person = {
      eyes: 2,
      head: 1
    }
    
    // 女人  构造函数   继承  想要 继承 Person
    function Woman() {
    }
    // Woman 通过原型来继承 Person
    Woman.prototype = Person
    // 指回原来的构造函数
    Woman.prototype.constructor = Woman
    
    const red = new Woman()
    console.log(red) // 吸烟这个方法,女人自动也添加
    
    // 男人 构造函数  继承  想要 继承 Person
    function Man() {
    }
    // 通过 原型继承 Person
    Man.prototype = Person
    Man.prototype.constructor = Man
    
    // 给男人添加一个方法 吸烟
    Man.prototype.smoke = function () {
      console.log('吸烟')
    }
    
    const pink = new Man()
    console.log(pink)
    

    image.png

  4. 解决:

    需求:男人和女人不要使用同一个对象,但是不同对象里面包含相同的属性和方法

    答案:构造函数

    new 每次都会创建一个新的对象

<body>
  <script>
    // 继续抽取   公共的部分放到原型上
    // 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)
  </script>
</body>

真正做这个案例,我们的思路应该是先考虑大的,后考虑小的

  1. 人类共有的属性和方法有那些,然后做个构造函数,进行封装,一般公共属性写到构造函数内部,公共方法,挂载到构造函数原型身上。

  2. 男人继承人类的属性和方法,之后创建自己独有的属性和方法

  3. 女人同理

原型链

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

image.png

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

查找规则

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。

  2. 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)

  3. 如果还没有就查找原型对象的原型(Object的原型对象)

  4. 依此类推一直找到 Object 为止(null)

  5. __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线

可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

console.log(ldh instanceof Person) // true
console.log(ldh instanceof Object) // true
console.log(ldh instanceof Array) // false
console.log([1, 2, 3] instanceof Array) // true
console.log(Array instanceof Object) // true

原型链是什么?原型链是一套查找规则,提供了查找属性和方法的一条路,像一条链子串起来,所以叫做原型链。那它是怎么查找的呢?(结合查找规则来说)先从自身的查找,没有,再从上一层查找,没有,再从上一层查...

注意:只要是对象就有__proto__,只要是原型对象就有constructor

综合案例

<body>
  <button id="delete">删除</button>
  <button id="login">登录</button>
  <script>
    // 1.  模态框的构造函数
    function Modal(title = '', message = '') {
      // 公共的属性部分
      this.title = title
      this.message = message
      // 因为盒子是公共的
      // 1. 创建 一定不要忘了加 this 
      this.modalBox = document.createElement('div')
      // 2. 添加类名
      this.modalBox.className = 'modal'
      // 3. 填充内容 更换数据
      this.modalBox.innerHTML = `
        <div class="header">${this.title} <i>x</i></div>
        <div class="body">${this.message}</div>
      `
      // console.log(this.modalBox)
    }
    // 2. 打开方法 挂载 到 模态框的构造函数原型身上
    Modal.prototype.open = function () {
      if (!document.querySelector('.modal')) {
        // 把刚才创建的盒子 modalBox  渲染到 页面中  父元素.appendChild(子元素)
        document.body.appendChild(this.modalBox)
        // 获取 x  调用关闭方法
        this.modalBox.querySelector('i').addEventListener('click', () => {
          // 箭头函数没有this 上一级作用域的this
          // 这个this 指向 m 
          this.close()
        })
      }
    }
    // 3. 关闭方法 挂载 到 模态框的构造函数原型身上
    Modal.prototype.close = function () {
      document.body.removeChild(this.modalBox)
    }

    // 4. 按钮点击
    document.querySelector('#delete').addEventListener('click', () => {
      const m = new Modal('温馨提示', '您没有权限删除')
      // 调用 打开方法
      m.open()
    })

    // 5. 按钮点击
    document.querySelector('#login').addEventListener('click', () => {
      const m = new Modal('友情提示', '您还么有注册账号')
      // 调用 打开方法
      m.open()
    })

  </script>
</body>

内置对象

内置对象是什么?

JavaScript内部提供的对象,包含各种属性和方法给开发者调用

  • JavaScript 中的对象分为3种:自定义对象 、内置对象、 浏览器对象
  • 前面两种对象是JS 基础 内容,属于 ECMAScript; 第三个浏览器对象属于我们JS 独有的, 我们JS API 讲解
  • 内置对象最大的优点就是帮助我们快速开发
  • JavaScript 提供了多个内置对象:Math、 Date 、Array、String等

查文档 MDN

学习一个内置对象的使用,只要学会其常用成员的使用即可,我们可以通过查文档学习,可以通过MDN/W3C来查询。

Mozilla 开发者网络(MDN)提供了有关开放网络技术(Open Web)的信息,包括 HTML、CSS 和万维网及HTML5 应用的 API。

MDN: developer.mozilla.org/zh-CN/

如何学习对象中的方法
  1. 查阅该方法的功能
  2. 查看里面参数的意义和类型
  3. 查看返回值的意义和类型
  4. 通过 demo 进行测试

Math对象

  • 介绍:Math对象是JavaScript提供的一个“数学”对象
  • 作用:提供了一系列做数学运算的方法
  • Math对象包含的方法有:
    • random:生成0-1之间的随机数(包含0不包括1)
    • ceil:向上取整
    • floor:向下取整
    • max:找最大数
    • min:找最小数
    • pow:幂运算
    • abs:绝对值

Math 对象不是构造函数,它具有数学常数和函数的属性和方法。跟数学相关的运算(求绝对值,取整、最大值等)可以使用 Math 中的成员。

Math.PI // 圆周率 属性
Math.floor() // 向下取整 方法
Math.ceil() // 向上取整
Math.round() // 四舍五入版 就近取整 注意 -3.5 结果是 -3
Math.abs() // 绝对值 有隐式转换,会把字符串型的-1转为数字型
Math.max()/Math.min() // 求最大和最小值

注意:上面的方法必须带括号

随机数方法 random()

random() 方法可以随机返回一个小数,其取值范围是 [0,1),左闭右开 0 <= x < 1 ,不跟参数

//得到一个两数之间的随机整数,包括两个数在内
function getRandom(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

日期对象

Date 概述
  • 日期对象:用来表示时间的对象

  • 作用:可以得到当前系统时间

  • Date 对象和 Math 对象不一样,它是一个构造函数,所以我们需要实例化后才能使用

Date()方法的使用
  • 在代码中发现了 new 关键字时,一般将这个操作称为实例化

  • 创建一个时间对象并获取时间

    • 获得当前时间 const date = new Date()
    • 获得指定时间 const date = new Date('2018-8-8')
日期格式化

使用场景:因为日期对象返回的数据我们不能直接使用,所以需要转换为实际开发中常用的格式

image.png

toLocaleString格式化时间
//2025/7/15 17:15:31
console.log(date.toLocaleString());
//2025/7/15
console.log(date.toLocaleDateString());
//17:15:31
console.log(date.toLocaleTimeString());
时间戳
  • 使用场景:如果计算倒计时效果,前面方法无法直接计算,需要借助于时间戳完成

  • 什么是时间戳:

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

    1970年1月1日是世界标准时间

  • 算法:将来的时间戳 - 现在的时间戳 = 剩余时间毫秒数

    • 剩余时间毫秒数 转换为 剩余时间的 年月日时分秒 就是 倒计时时间
    • 比如 将来时间戳 2000ms - 现在时间戳 1000ms = 1000ms
    • 1000ms 转换为就是 0小时0分1秒
  • 我们经常利用总的毫秒数来计算时间,因为它更精确

  • 三种方式获取时间戳

    • 使用 getTime() 方法 (new Date()).getTime()
    • 简写 +new Date()
    • 使用 Date.now() 无需实例化,但是只能得到当前的时间戳, 而前面两种可以返回指定时间的时间戳
// 实例化Date对象
const now = new Date()
// 1. 用于获取对象的原始值
console.log(date.valueOf()) // 现在时间距离1970.1.1总的毫秒数
console.log(date.getTime())
// 2. 简单写可以这么做(最常用的写法)
const now = +new Date()
// 3. HTML5中提供的方法,有兼容性问题
const now = Date.now()

数组对象

数组对象的创建

创建数组对象的两种方式

  • 字面量方式
  • new Array()
//字面量方式
var arr = [1, 2, 3];
console.log(arr);
//new Array()
var arr1 = new Array(); // 创建了一个空的数组
var arr2 = new Array(2); // 这个2表示数组的长度是2,里面有2个空的数组元素
var arr2 = new Array(2, 3); // 等价于 [2,3]
检测是否为数组
  • instanceof 运算符,可以判断一个对象是否属于某种类型
  • Array.isArray()用于判断一个对象是否为数组,isArray() 是 HTML5 中提供的方法
var arr = [1, 23];
var obj = {};
console.log(arr instanceof Array); // true
console.log(obj instanceof Array); // false
console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false
添加删除数组元素的方法

image.png

数组排序

image.png

var arr = [1, 64, 9, 6];
arr.sort(function(a, b) {
    return b - a; // 降a序
    // return a - b; // 升序
});
console.log(arr);
数组索引方法

image.png

数组转换为字符串

image.png

其他

image.png

字符串对象

基本包装类型

为了方便操作基本数据类型,JavaScript 还提供了三个特殊的引用类型:String、Number和 Boolean。

基本包装类型就是把简单数据类型包装成为复杂数据类型,这样基本数据类型就有了属性和方法。

// 下面代码有什么问题?
var str = 'andy';
console.log(str.length);

按道理基本数据类型是没有属性和方法的,而对象才有属性和方法,但上面代码却可以执行,这是因为 js 会把基本数据类型包装为复杂数据类型,其执行过程如下:

// 1. 生成临时变量,把简单类型包装为复杂数据类型
var temp = new String('andy');
// 2. 赋值给我们声明的字符变量
str = temp;
// 3. 销毁临时变量
temp = null;
字符串的不可变

指的是里面的值不可变,虽然看上去可以改变内容,但其实是地址变了,内存中新开辟了一个内存空间。

var str = 'abc';
str = 'hello'
// 当重新给 str 赋值的时候,常量'abc'不会被修改,依然在内存中
// 重新给字符串赋值,会重新在内存中开辟空间,这个特点就是字符串的不可变
// 由于字符串的不可变,在大量拼接字符串的时候会有效率问题
var str = '';
for (var i = 0; i < 100000; i++) {
    str += i;
}
console.log(str); // 这个结果需要花费大量时间来显示,因为需要不断的开辟新的空间
根据字符返回位置

字符串所有的方法,都不会修改字符串本身(字符串是不可变的),操作完成会返回一个新的字符串。

image.png

//查找字符串"abcoefoxyozzoppo"中所有o出现的位置以及次数
var str = 'abcoefoxyozzoppo';
var index = str.indexOf('o');
var num = 0;
while(index !== -1) {
    console.log(index);
    num++;
    index = str.indexOf('o', index+1)
}
console.log('o出现的次数为:'+ num);
//结果是:3 6 9 12 15 o出现的次数为:5
根据位置返回字符

image.png

//判断一个字符串 'abcoefoxyozzopp' 中出现次数最多的字符,并统计其次数。
var str = 'abcoefoxyozzopp';
var obj = {};
for (let i = 0; i < str.length; i++) {
    var num = 0;
    var char = str.charAt(i)
    if (obj[char]) {
        obj[char]++;
    } else {
        obj[char] = 1;
    }
}
var max = 0;
var ch = '';
for (var k in obj) {
    if (obj[k] > max) {
        max = obj[k];
        ch = k;
    }
}
console.log('出现次数最多的字符是:'+ ch + ',次数为:' + max);

image.png

字符串操作方法

image.png

replace()方法

replace() 方法用于在字符串中用一些字符替换另一些字符。只会替换第一次出现的字符。

其使用格式为:replace(被替换的字符串, 要替换为的字符串);

//将字符串'abcoefoxyozzoppo'中所有的'o'替换为'*'
var str = 'abcoefoxyozzoppo';
while (str.indexOf('o') !== -1) {
    str = str.replace('o', '*')
}
console.log(str);
split()方法

split()方法用于切分字符串,它可以将字符串切分为数组。在切分完毕之后,返回的是一个新数组。

var str = 'a,b,c,d';
console.log(str.split(',')); // 返回的是一个数组 [a, b, c, d]
其他
  • toUpperCase() // 转换大写
  • toLowerCase() // 转换小写
给定一个字符串,如:“abaasdffggghhjjkkgfddsssss3444343”,问题如下:

1、 字符串的长度

2、 取出指定位置的字符,如:0,3,5,93、 查找指定字符是否在以上字符串中存在,如:i,c ,b等

4、 替换指定的字符,如:g替换为22,ss替换为b等操作方法

5、 截取指定开始位置到结束位置的字符串,如:取得1-5的字符串

6、 找出以上字符串中出现次数最多的字符和出现的次数

7、 遍历字符串,并将遍历出的字符两头添加符号“@

高级技巧

深浅拷贝

开发中我们经常需要复制一个对象。如果直接用赋值会有下面问题:

image.png

浅拷贝

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

浅拷贝:拷贝的是地址

常见方法:

  1. 拷贝对象:

    Object.assgin()

    展开运算符 {...obj} 拷贝对象

  2. 拷贝数组

    Array.prototype.concat()

    [...arr]

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

深拷贝

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

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

常见方法:

  1. 通过递归实现深拷贝
  2. lodash/cloneDeep
  3. 通过JSON.stringify()实现
递归实现深拷贝

函数递归:

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

  • 简单理解:函数内部自己调用自己, 这个函数就是递归函数
  • 递归函数的作用和循环效果类似
  • 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
<body>
  <script>
    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)
  </script>
</body>

如何实现深拷贝?

  • 深拷贝做到拷贝出来的新对象不会影响旧对象,要想实现深拷贝用到函数递归
  • 当我们普通拷贝的时候,没问题,直接赋值就行
  • 如果遇到数组,再次调用递归函数就可以了
  • 如果遇到对象形式,再次利用递归函数,把对象解决
  • 先array数组后对象
js库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>
通过JSON(JSON.stringify())实现
<body>
  <script>
    const obj = {
      uname: 'pink',
      age: 18,
      hobby: ['乒乓球', '足球'],
      family: {
        baby: '小pink'
      }
    }
    // 把对象转换为 JSON 字符串
    // console.log(JSON.stringify(obj))
    const o = JSON.parse(JSON.stringify(obj))
    console.log(o)
    o.family.baby = '123'
    console.log(obj)
  </script>
</body>

总结

  1. 直接赋值和浅拷贝有什么区别?

    直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址

    浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会相互影响

  2. 浅拷贝怎么理解?

    拷贝对象之后,里面的属性值是简单数据类型直接拷贝值

    如果属性值是引用数据类型则拷贝的是地址

  3. 实现深拷贝三种方式?

    自己利用递归函数书写深拷贝

    利用js库 lodash 里面的 _.cloneDeep()

    利用JSON字符串转换

异常处理

了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。

throw 抛异常

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

<script>
  function counter(x, y) {

    if(!x || !y) {
      // throw '参数不能为空!';
      throw new Error('参数不能为空!')
    }

    return x + y
  }

  counter()
</script>

总结:

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 throw 使用,能够设置更详细的错误信息

try/catch 捕获异常

我们可以通过try / catch 捕获错误信息(浏览器提供的错误信息) try 试试 catch 拦住 finally 最后

<script>
   function foo() {
      try {
        //
        // 查找 DOM 节点
        const p = document.querySelector('.p')
        p.style.color = 'red'
      } catch (error) {
        // try 代码段中执行有错误时,会执行 catch 代码段
        // 查看错误信息
        console.log(error.message)
        // 终止代码继续执行
        return

      }
      finally {
          alert('执行')
      }
      console.log('如果出现错误,我的语句不会执行')
    }
    foo()
</script>

总结:

  1. try...catch 用于捕获错误信息
  2. 将预估可能发生错误的代码写在 try 代码段中
  3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
  4. finally 不管是否有错误,都会执行

debugger

相当于断点调试

image.png

总结

  1. 抛出异常我们用那个关键字?它会终止程序吗?

    throw 关键字

    会中止程序

  2. 抛出异常经常和谁配合使用?

Error 对象配合 throw 使用

  1. 捕获异常我们用那3个关键字?可能会出现的错误代码写到谁里面

    try catch finally

    try

  2. 怎么调用错误信息?

    利用catch的参数

处理this

了解函数中 this 在不同场景下的默认值,知道动态指定函数 this 值的方法。

this指向

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

普通函数

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

<script>
  // 普通函数
  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()
</script>
<body>
  <button>点击</button>
  <script>
    // 普通函数:谁调用我,this就指向谁
    
    //最外层 this 指向 window
    console.log(this)  // window
    
    //普通函数 this指向window,默认是window调用的(注意:开启严格模式,函数里的this是undefined)
    function fn() {
      console.log(this)  // window    
    }
    window.fn()
    
    //定时器 thi指向window,默认是window调用的
    window.setTimeout(function () {
      console.log(this) // window 
    }, 1000)
   
   // 指向调用者 document.querySelector('button').addEventListener('click', function () {
      console.log(this)  // 指向 button
    })
    
    // 指向调用者
    const obj = {
      sayHi: function () {
        console.log(this)  // 指向 obj
      }
    }
    obj.sayHi()
  </script>
</body>

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

箭头函数

箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存 this

  1. 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的

  2. 箭头函数中的this引用的就是最近作用域中的this

  3. 向外层作用域中,一层一层查找this,直到有this的定义

箭头函数中访问的 this 不过是箭头函数所在作用域的 this 变量。

<script>
    
  console.log(this) // 此处为 window
  // 箭头函数
  const sayHi = function() {
    console.log(this) // 该箭头函数中的 this 为函数声明环境中 this 一致
  }
  // 普通对象
  const user = {
    name: '小明',
    // 该箭头函数中的 this 为函数声明环境中 this 一致
    walk: () => {
      console.log(this)
    },
    
    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()
</script>

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

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

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

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

总结:

  1. 函数内不存在this,沿用上一级的

  2. 不适用 构造函数,原型函数,dom事件函数等等

  3. 适用 需要使用上层this的地方

  4. 使用正确的话,它会在很多地方带来方便,后面我们会大量使用慢慢体会

总结
  1. 普通函数this指向我们怎么记忆?

    【谁调用, this 的值指向谁】,没有调用者指向window,严格模式下指向undefined

  2. 箭头函数的this记忆? 箭头函数内不存在this,沿用上一级的,过程:向外层作用域中,一层一层查找this,直到有this的定义

  3. 箭头函数不适用和使用的情况?

    不适用:构造函数,原型函数,字面量对象中函数,dom事件函数

    适用:需要使用上层this的地方

改变this指向

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

  • call()
  • apply()
  • bind()
call

使用 call 方法调用函数,同时指定被调用函数中 this 的值

语法:fun.call(thisAg, arg1, arg2,...)

  • thisArg:在 fun 函数运行时指定的 this 值
  • arg1,arg2:传递的其他参数
  • 返回值就是函数的返回值,因为它就是调用函数

使用方法如下代码所示:

<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);
  console.log(result);
</script>

总结:

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

使用 apply 方法调用函数,同时指定被调用函数中 this 的值。

语法:fun.apply(thisArg, [argsArray])

  • thisArg:在fun函数运行时指定的 this 值
  • argsArray:传递的值,必须包含在数组里面
  • 返回值就是函数的返回值,因为它就是调用函数
  • 因此 apply 主要跟数组有关系,比如使用 Math.max() 求数组的最大值

使用方法如下代码所示:

<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

  // 求和函数,注意这里的x并不是整个数组,而是数组中的第一个元素值
  function counter(x, y) {
    return x + y
  }
  // 调用 counter 函数,并传入参数
  let result = counter.apply(null, [5, 10])
  console.log(result) //15
</script>
//求数组最大值2个方法:
const arr = [3, 5, 2, 9]
//方法一:利用apply
console.log(Math.max.apply(null, arr))
//方法二:利用扩展运算符
console.log(Math.max(...arr))

总结:

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

bind 方法并不会调用函数,而是创建一个指定了 this 值的新函数

语法: fun.bind(thisArg, arg1, arg2, ...)

  • thisArg:在 fun 函数运行时指定的 this 值
  • arg1,arg2:传递的其他参数
  • 返回由指定的 this 值和初始化参数改造的 原函数拷贝 (新函数)
  • 因此当我们只是想改变 this 指向,并且不想调用这个函数的时候,可以使用 bind,比如改变定时器内部的this指向

使用方法如下代码所示:

<script>
  // 普通函数
  function sayHi() {
    console.log(this)
  }
  let user = {
    name: '小明',
    age: 18
  }
  // 调用 bind 指定 this 的值
  let sayHello = sayHi.bind(user);
  // 调用使用 bind 创建的新函数
  sayHello()
</script>
// 需求,有一个按钮,点击里面就禁用,2秒钟之后开启
document.querySelector('button').addEventListener('click', function () {
  // 禁用按钮
  this.disabled = true
  window.setTimeout(function () {
    // 在这个普通函数里面,我们要this由原来的window 改为 btn
    this.disabled = false
  }.bind(this), 2000)   // 这里的this 和 btn 一样
})

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

总结
  1. call的作用是?

    调用函数,并可以改变被调用函数里面的this指向

    call 里面第一个参数是 指定this, 其余是实参,传递的参数

    整体做个了解,后期用的很少

    fn.call(obj, 1, 2)

  2. call和apply的区别是?

    都是调用函数,都能改变this指向

    参数不一样,apply传递的必须是数组

  3. call apply bind 总结

  • 相同点:

    都可以改变函数内部的this指向.

  • 区别点:

    call 和 apply 会调用函数, 并且改变函数内部this指向.

    call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2..形式,apply 必须数组形式[arg]

    bind 不会调用函数, 可以改变函数内部this指向.

  • 主要应用场景:

    call 调用函数并且可以传递参数

    apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值

    bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向

性能优化

防抖

防抖(debounce) 所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

简单来说,防抖就是单位时间内,频繁触发事件,只执行最后一次。

image.png

举个例子:王者荣耀回城,只要被打断旧需要重新来。

使用场景:搜索框输入

  • 假设搜索框输入就可以发送请求,但是不能每次输入都去发送请求,输入比较快发送请求会比较多,我们设定一个时间,假如300ms, 当输入第一个字符时候,300ms后发送请求,但是在200ms的时候又输入了一个字符,则需要再等300ms 后发送请求
  • 手机号、邮箱验证输入检测

实现

  • 利用lodash库实现防抖 _.debounce(fun, 时间)
  • 手写防抖函数
    • 核心是利用 setTimeout 定时器来实现
    • 1.声明定时器变量
    • 2.每次鼠标移动(事件触发)的时候都要先判断是否有定时器,如果有先清除以前的定时器
    • 3.如果没有定时器,则开启定时器,存入到定时器变量里面
    • 4.定时器里面写函数调用
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .box {
        width: 300px;
        height: 300px;
        background-color: pink;
      }
    </style>
  </head>
  <body>
    <div class="box"></div>
    <script src="./lodash.min.js"></script>
    <script>
      //鼠标滑过盒子显示文字
      const box = document.querySelector(".box")

      let i = 1
      function mouseMove() {
        box.innerHTML = i++
      }
      
      //方式一:【普通版】
      box.addEventListener("mousemove", mouseMove)
      
      //方式二:利用lodash库实现防抖 _.debounce(fun, 时间)
      box.addEventListener("mousemove", _.debounce(mouseMove, 500))
      
      //方式三:手写防抖函数
      function debounce(fn, t) {
        let timer
        // return 返回一个匿名函数
        return function () {
          if (timer) clearTimeout(timer)
          timer = setTimeout(function () {
            fn()
          }, t)
        }
      }
      box.addEventListener("mousemove", debounce(mouseMove, 500))
    </script>
  </body>
</html>

节流

节流(throttle):单位时间内,频繁触发事件,只执行一次

image.png

举个例子:王者荣耀技能冷却,期间无法继续释放技能。

使用场景:轮播图点击效果、鼠标移动 mousemove、页面尺寸缩放resize、滚动条滚动scroll等

  • 假如一张轮播图完成切换需要300ms, 不加节流效果,快速点击,则嗖嗖嗖的切换,加上节流效果, 不管快速点击多少次, 300ms时间内,只能切换一张图片。

实现

  • lodash 提供的节流函数 _.throtte(fun, 时间)
  • 手写一个节流函数
    • 核心是利用 setTimeout 定时器来实现
    • 1.声明定时器变量
    • 2.每次鼠标移动(事件触发)的时候都要先判断是否有定时器,如果有定时器则不开启新定时器
    • 3.如果没有定时器,则开启定时器,记得存入到定时器变量里面
    • 3.1.定时器里面调用执行函数
    • 3.1.定时器里面要把定时器清空
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .box {
        width: 300px;
        height: 300px;
        background-color: pink;
      }
    </style>
  </head>
  <body>
    <div class="box"></div>
    <script src="./lodash.min.js"></script>
    <script>
      //鼠标滑过盒子显示文字
      const box = document.querySelector(".box")

      let i = 1
      function mouseMove() {
        box.innerHTML = i++
      }
      
      //方式一:【普通版】
      box.addEventListener("mousemove", mouseMove)
      
      //方式二:利用lodash库实现节流 _.debounce(fun, 时间)
      box.addEventListener("mousemove", _.throtte(mouseMove, 500))
      
      //方式三:手写节流函数
      function throttle(fn, t) {
        let timer = null
        // return 返回一个匿名函数
        return function () {
          if (!timer) {
              timer = setTimeout(function () {
                fn()
                timer = null
              }, t)
          }
        }
      }
      box.addEventListener("mousemove", throttle(mouseMove, 500))
    </script>
  </body>
</html>

总结

  1. 防抖是什么?

    单位时间内,频繁触发事件,只执行最后一次。

  2. 防抖有什么使用场景?

    搜索框搜索输入。只需用户最后一次输入完,再发送请求。

    手机号、邮箱验证输入检测

  3. 节流是什么?

    单位时间内,频繁触发事件,只执行一次

    简单理解:在 500ms 内,不管触发多少次事件,只执行一次

  4. 节流有什么使用场景?

高频事件:鼠标移动 mousemove、页面尺寸缩放resize、滚动条滚动scroll

  1. 防抖和节流总结?

image.png

扩展阅读

Js前面需要加分号的情况

  1. 立即执行函数

    (function () { })();

    或者

    ;(function () { })()

  2. 数组结构,或者说是数组开头的,特别是前面有语句的一定注意加分号

    ;[b, a] = [a, b]

    ;[1,2].map()

视频播放事件

  • ontimeupdate 事件在视频/音频(audio/video)当前的播放位置发送改变时触发
  • onloadeddata 事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频(audio/video)的下一帧时触发
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="referrer" content="never" />
    <title>页面打开,可以记录上一次的视频播放位置</title>
    <style>
      * {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
      }

      .container {
        width: 1200px;
        margin: 0 auto;
      }

      .video video {
        width: 100%;
        padding: 20px 0;
      }

      .elevator {
        position: fixed;
        top: 280px;
        right: 20px;
        z-index: 999;
        background: #fff;
        border: 1px solid #e4e4e4;
        width: 60px;
      }

      .elevator a {
        display: block;
        padding: 10px;
        text-decoration: none;
        text-align: center;
        color: #999;
      }

      .elevator a.active {
        color: #1286ff;
      }

      .outline {
        padding-bottom: 300px;
      }
    </style>
  </head>

  <body>
    <div class="container">
      <div class="header">
        <a href="http://pip.itcast.cn">
          <img src="https://pip.itcast.cn/img/logo_v3.29b9ba72.png" alt="" />
        </a>
      </div>
      <div class="video">
        <video src="https://v.itheima.net/LapADhV6.mp4" controls></video>
      </div>
      <div class="elevator">
        <a href="javascript:;" data-ref="video">视频介绍</a>
        <a href="javascript:;" data-ref="intro">课程简介</a>
        <a href="javascript:;" data-ref="outline">评论列表</a>
      </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
    <script>
      const video = document.querySelector("video")
      //addEventListener和on的写法都可以
      // video.addEventListener(
      //   "timeupdate",
      //   _.throttle(function () {
      //     console.log(111);
      //   }, 1000)
      // )
      video.ontimeupdate = _.throttle(function () {
        localStorage.setItem("currentTime", video.currentTime)
      }, 1000)

      video.onloadeddata = function () {
        video.currentTime = localStorage.getItem("currentTime") || 0
      }
    </script>
  </body>
</html>