JavaScript 进阶

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

1、作用域

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

1.1 局部作用域

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

函数作用域

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

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

总结:

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

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

1.4 垃圾回收机制

1.4.1 垃圾回收机制

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

内存的生命周期

  • 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存。
  • 内存使用:读写内存,使用变量和函数等。
  • 内存回收:使用完毕,由垃圾回收器自动回收。
    • 全局变量一般不会回收(页面关闭的时候回收)
    • 局部变量的值,不用了,会被自动回收掉

内存泄漏:程序中分配的内存由于某种原因未释放或无法释放叫做内存泄露。(两个对象相互引用,互指,尬住了。)

1.4.2 垃圾回收算法说明

堆栈空间分配区别:

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

(2)堆:一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放在堆中

引用计数法:看一个对象是否有指向它的引用,没有引用了就回收对象。

JavaScript高级学习-LMLPHP

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

1.4 闭包

函数内嵌套了一层函数,外层函数用来定义局部变量,从而使得外部无法访问内部数据,达到数据私有化。内层函数使用外层函数定义的变量执行响应的操作,外层函数需要返回内层函数。

<body>
  <script>
    // 1. 闭包 : 内层函数 + 外层函数变量
    // function outer() {
    //   const a = 1
    //   function f() {
    //     console.log(a)//使用了外层函数的变量
    //   }
    //   f()
    // }
    // outer()
	 function outer(){
            let a =10
            function fn(){
                console.log(a)
            }
            return fn  //返回的fn是一个函数,调用outer()返回一个函数fn。所以outer() = fn = function fn() {}
        }
        const fun = outer() //相当于const fun = function fn(){}
        fun()
    // 2. 闭包的应用: 实现数据的私有。统计函数的调用次数
    // let count = 1
    // function fn() {
    //   count++
    //   console.log(`函数被调用${count}次`)
    // }

    // 3. 闭包的写法  统计函数的调用次数
    function outer() {
      let count = 1
      function fn() {
        count++
        console.log(`函数被调用${count}次`)
      }
      return fn
    }
    const re = outer()
    // const re = function fn() {
    //   count++
    //   console.log(`函数被调用${count}次`)
    // }
    re()
    re()
    // const fn = function() { }  函数表达式
    // 4. 闭包存在的问题: 可能会造成内存泄漏
  </script>
</body>

总结:

1.怎么理解闭包?

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

2.闭包的作用?

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

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

  • 内存泄漏

1.5 变量提升

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

<script>
  // 访问变量 str
  console.log(str + 'world!');

  // 声明变量 str
  var str = 'hello ';
</script>

变量提升流程:

  • 先把var变量提升到当前作用域最前面
  • 只提升变量变量声明,不提升变量赋值。
  • 然后依次执行代码

总结:

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

2、函数

2.1 函数提升

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

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

  // 不存在提升现象
  bar()  // 错误
  var bar = function () {
    console.log('函数表达式不存在提升现象...')    //函数表达式必须先声明和赋值,后调用。
  }
</script>

总结:

  1. 函数提升能够使函数的声明调用更灵活
  2. 函数表达式不存在提升的现象
  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 的作用是动态获取函数的实参
  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>

总结:

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

展开运算符

将一个数组展开。不会修改原数组。可以应用于求取最大值,最小值。

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

2.3 箭头函数

箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。适用于本来需要匿名函数的地方。

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

  </script>
</body>

总结:

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

2.3.1 箭头函数参数

箭头函数中没有 arguments,只能使用 ... 动态获取实参

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

2.3.2 箭头函数 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() //不是window调用fn(),而是方法体作用域中无this,进而找上一层。上一层是window
    
     // 对象方法箭头函数 this
     const obj = {
       uname: 'pink老师',
       sayHi: () => {
         console.log(this)  // this 指向谁? window
       }
     }
     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>

JavaScript高级学习-LMLPHP

3、解构赋值

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

3.1 数组解构

数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:

<script>
  // 普通的数组
  let arr = [1, 2, 3]
  // 批量声明变量 a b c 
  // 同时将数组单元值 1 2 3 依次赋值给变量 a b c
  let [a, b, c] = arr = [1, 2, 3]
  console.log(a); // 1
  console.log(b); // 2
  console.log(c); // 3
    
  const [a,b,..c] = [1,2,3,4]
  console.log(c) //[3,4]
</script>

总结:

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

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

js前面必须加分号情况

1.立即执行函数

(function t(){})()

2.数组解构

3.2 对象解构

对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:

<script>
  // 普通对象
  const user = {
    name: '小明',
    age: 18
  };
  // 批量声明变量 name age
  // 同时将数组单元值 小明  18 依次赋值给变量 name  age
  const {name, age} = user

  console.log(name) // 小明
  console.log(age) // 18
    
    
  //数组对象解构
   const pig = [
            {
                uname:'piqi',
                age:11
            }
        ]
   const [{uname,age} ]= pig
</script>

总结:

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

4、遍历数组

4.1 forEach遍历数组

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。适合遍历数组对象

<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) // 索引号 0 1 2
    })
    // console.log(result) undefined
  </script>
</body>

JavaScript高级学习-LMLPHP

4.2 filter筛选数组

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

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

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

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

4.3 for...of

const arr = ['red', 'green', 'pink']
        for(const elemnet of arr) {
            console.log(elemnet);    //red green pink
        } 

4.4 map()

map()方法对数组中的每个元素都调用给定的函数,并返回一个新的数组,其中包含处理后的结果。该方法不会修改原始数组。

const arr = ['red', 'green', 'pink']
const result = arr.map(function (element,index) {
  		console.log(element);// 数组元素 red  green pink
        console.log(index);// 索引号 0 1 2
       })


const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(function(number) {
  return number * 2;
});

console.log(doubledNumbers); // 输出: [2, 4, 6, 8, 10]

5、数组添加元素

  1. push() 方法:

    const array = [1, 2, 3];
    array.push(4);
    

    push() 方法将一个或多个元素添加到数组的末尾,并返回新数组的长度。在上述示例中,我们将数字 4 添加到数组 array 的末尾。

  2. unshift() 方法:

    const array = [2, 3, 4];
    array.unshift(1);
    

    unshift() 方法将一个或多个元素添加到数组的开头,并返回新数组的长度。在上述示例中,我们将数字 1 添加到数组 array 的开头。

  3. concat() 方法:

    const array1 = [1, 2, 3];
    const array2 = [4, 5, 6];
    const newArray = array1.concat(array2); //[1,2,3,4,5,6]
    

    concat() 方法将一个或多个数组与原数组合并创建一个新数组,并返回该新数组。在上述示例中,我们将数组 array2 连接到数组 array1 的后面,形成一个新数组 newArray

  4. 使用索引直接赋值:

    const array = [1, 2, 3];
    array[3] = 4;
    

    在 JavaScript 中,我们可以通过指定索引来直接赋值来添加元素。上述示例中,我们通过将数字 4 赋值给索引为 3 的位置来向数组中添加元素。

  5. 使用 splice() 方法:

    const array = [1, 2, 3];
    array.splice(1, 0, 4);  //[1,4,2,3]
    

    splice() 方法可以实现在指定位置插入一个或多个元素,并删除指定数量的元素。上述示例中,我们在索引位置 1 插入数字 4,同时不删除任何元素。

6、渲染商品案例

<!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">
  <title>商品渲染</title>
  <style>
  </style>
</head>

<body>
  <div class="list">
    <!-- <div class="item">
      <img src="" alt="">
      <p class="name"></p>
      <p class="price"></p>
    </div> -->
  </div>
  <script>
    const goodsList = [
      {
        id: '4001172',
        name: '称心如意手摇咖啡磨豆机咖啡豆研磨机',
        price: '289.00',
        picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg',
      },
    ]

    let str = ''
    goodsList.forEach(item => {
      const{name, price, picture} = item
      str += `
      <div class="item">
        <img src="${price}" alt="">
        <p class="name">${name}</p>
        <p class="price">${price}</p>
      </div>
      `
    })
    document.querySelector('.list').innerHTML = str
  </script>
</body>

</html>

7、深入对象

7.1 创建对象

//1.利用对象字面量创建对象
const o = {
    name:'佩奇'
}

//2.利用new Object 创建对象
const 0 = new Object({name:'佩奇'})
console.log(o) //{name:'佩奇'}

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

7.2 构造函数

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

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

命名以大写字母开头,只能用new 操作符来执行

function Pig(name,age,gender){
      this.name = name;
      this.age = age;
      this.gender = gender;
    }
    // 创建佩奇对象
    const Peppa = new Pig('佩奇',6,'女')
    console.log(Peppa);

JavaScript高级学习-LMLPHP

总结:

  1. 使用 new 关键字调用函数的行为被称为实例化
  2. 实例化构造函数时没有参数时可以省略 ()
  3. 构造函数的返回值即为新创建的对象
  4. 构造函数内部的 return 返回的值无效!
  5. new Object() 也是实例化构造函数

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

构造函数的作用是来创建多个类似的对象,大写字母开头的函数。new 关键字调用函数的行为被称为实例化。 构造函数内部不需要写return,构造函数自动返回创建的新的对象。

7.3 实例化执行过程

  1. new=>创建新对象(空的)
  2. 构造函数this指向新(空)对象
  3. 执行构造函数代码,修改this,添加新的属性
  4. 返回新对象

JavaScript高级学习-LMLPHP

7.4 实例成员

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

  • 为构造函数传入参数,创建结构相同但值不同的对象

  • function Pig(name,age,gender){
          this.name = name;
    
        }
        const Peppa = new Pig('佩奇',6,'女')  //Peppa.name ='小主佩奇' 实例属性
        const John = new Pig('约翰',8,'男')
        Peppa.sayHi=()=>{clg}  //实例方法
    
  • 构造函数创建的实例对象彼此独立互不影响

<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. 为构造函数传入参数,动态创建结构相同但值不同的对象

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

7.5 静态成员

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

  • 静态成员只能构造函数来访问
  • 静态方法中的this指向构造函数
<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 指向构造函数本身

8、内置构造函数

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

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

<script>
  // 实例化
	let date = new Date();
  
  // date 即为实例对象
  console.log(date);
</script>

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

Object

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

    const o = {name:'佩奇',age:6}
    // 获取对象的所有键,并且返回是一个数组
    const arr = Object.keys(o)
    console.log(arr); //['name','age']
	console.log(Object.values(o)); //['pink',18]
	const obj = {}
    Object.assign(obj,o) //将对象o中的拷贝到obj中
	Object.assign(o,{gender:'女'}) //向o中添加属性
<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>

总结:

  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>

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

总结:

  • arr.length:获取数组长度

  • const clothing = ['shoes', 'shirts', 'socks', 'sweaters'];
    console.log(clothing.length);  //4
    
  • arr.push(element1, ..., elementN):向数组末尾添加一个或多个元素,并返回新的长度

  • arr.pop():删除并返回数组的最后一个元素

  • arr.unshift(element1, ..., elementN):向数组开头添加一个或多个元素,并返回新的长度

  • arr.shift():删除并返回数组的第一个元素

  • arr.concat(array1, array2, ..., arrayN):连接两个或多个数组,并返回新的数组

  • arr.slice(startIndex, endIndex):截取数组的一部分并返回新的数组,不修改原数组

  • arr.splice(startIndex, deleteCount, item1, ..., itemN):从指定位置删除或替换元素,并返回被删除的元素组成的数组

  • arr.indexOf(searchElement, startIndex):查找指定元素在数组中首次出现的索引,如果不存在则返回-1

  • arr.lastIndexOf(searchElement, startIndex):查找指定元素在数组中最后一次出现的索引,如果不存在则返回-1

  • arr.includes(searchElement, startIndex):判断数组是否包含指定元素,返回布尔值

  • arr.find(callback):返回数组中满足条件的第一个元素,如果找不到则返回 undefined

    
    
  • arr.findIndex(callback):返回数组中满足条件的第一个元素的索引,如果找不到则返回 -1

  • arr.filter(callback):返回数组中满足条件的所有元素组成的新数组

  • arr.map(callback):根据回调函数的返回值对每个元素进行操作,并返回新的数组

  • arr.reduce(callback, initialValue):从左到右对数组的每个元素进行累积计算,并返回最终结果

  • arr.reduce(function(上一次值,当前值){},初始值)
    
    const arr = [1,5,8]
    const total = arr.reduce(function(prev,current) {return prev + current},10)
    //total的值无初始值10,等于14。
    
  • arr.reverse():颠倒数组中元素的顺序,修改原数组

  • arr.sort(compareFunction):对数组元素进行排序,修改原数组

  • arr.forEach(callback):对数组的每个元素执行一次回调函数,没有返回值

  • arr.every(callback):判断数组中的所有元素是否都满足指定条件,返回布尔值

  • arr.some(callback):判断数组中是否有至少一个元素满足指定条件,返回布尔值

  • arr.join(separator):将数组的所有元素连接成一个字符串,并使用指定的分隔符分隔

  • arr.toString():将数组转换为字符串,并返回结果

  • arr.includes(element, startIndex):判断数组是否包含指定元素,返回布尔值

  • arr.fill(value, startIndex, endIndex):用指定的值填充数组中的元素

  • Array.from(arrayLike[, mapFn[, thisArg]]):从类数组对象或可迭代对象创建一个新数组

  • Array.isArray(obj):检查一个值是否为数组类型,返回布尔值

包装类型

在 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.length)
  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() 用于设置保留小数位的长度

9、编程思想

9.1 面向过程

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

调用就可以了。

  • 性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机。
  • 不易维护,复用,扩展

9.2 面向对象

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

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

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

面向对象的特性:

  • 封装性

  • 继承性

  • 多态性

JavaScript高级学习-LMLPHP

10、构造函数

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

//构造函数  公共的属性和方法 封装到Star函数中
function Star(uname,age){
    this.uname = uname
    this.age = age
    this.sing = function(){
        console.log("嫦娥")
    }
}
const ldh = new Star('刘德华',54)
const zxy = new Star('张学友',54)
//浪费内存
<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 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之

间是彼此不影响的

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

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

11、原型对象

  • JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象。
  • 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存。
  • 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
  • 构造函数和原型对象中的this 都指向实例化的对象
  • 构造函数通过原型分配的函数是所有对象所共享的。
<script>
//构造函数  公共的属性写在构造函数中
function Star(uname,age){
    this.uname = uname
    this.age = age
    //this.sing = function(){
        //console.log("嫦娥")
    //}
}
//公共的方法挂载到原型对象上
   Star.prototype.sing = function(){
       console.log("嫦娥")
   } 
const ldh = new Star('刘德华',54)
const zxy = new Star('张学友',54)
ldh.sing() 
zxy.sing()
// ldh.sing === zxy.sing     true
</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>

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

11.1 原型-this指向

构造函数中 this 指向实例对象,原型上的方法指向也是实例对象

let that
    function Star(uname){
      console.log('this',this);
      that = this  //将Star中的this赋给that
      this.uname = uname
    }
    // 实例对象 ldh
    // 构造函数中的 this 就是 实例对象ldh
    const ldh = new Star('刘德华')
    console.log(that===ldh); 

JavaScript高级学习-LMLPHP


let that
    function Star(uname){
      // console.log('this',this)
      // that = this  //将Star中的this赋给that
      this.uname = uname
    }
    // 原型对象中的函数 this 指向还是实例对象ldh
    Star.prototype.sing = function(){
      console.log('this',this)
      that = this
      console.log('嫦娥');
    }
    //实例对象 ldh
    // 构造函数中的 this 就是 实例对象ldh
    const ldh = new Star('刘德华')
    ldh.sing() //谁调用,就指向谁!
    console.log(that===ldh);

JavaScript高级学习-LMLPHP

//最大值
const  arr=[1,2,3]
Array.prototype.max = function(){
    //展开运算符
    return Math.max(...this)
    //原型函数中的this指向 实例对象 arr
}
console.log(arr.max())

//数组求和
Array.prototype.sum = function(){
    return this.reduce((prev,item) => prev+item,0)
}
console.log([1,2,3].sum())

11.2 constructor 属性

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

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

JavaScript高级学习-LMLPHP

使用场景:

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

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

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

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

JavaScript高级学习-LMLPHP

11.3 对象原型

对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype

原型对象的属性和方法,就是因为对象有 proto 原型的存在。

JavaScript高级学习-LMLPHP

function Star(){}
const ldh = new Star()
//对象原型 __proto__ 指向该构造函数的原型对象
//ldh.__proto__  === Star.prototype    true

注意:

  • __proto__ 是JS非标准属性
  • [[prototype]]和__proto__意义相同
  • 用来表明当前实例对象指向哪个原型对象prototype
  • __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数

JavaScript高级学习-LMLPHP

JavaScript高级学习-LMLPHP

总结:

  1. prototype是原型(原型对象),构造函数(construct)都自动有原型。
  2. prototype原型和对象原型__proto__里面都有,都指向创建实例对象、原型的构造函数。
  3. __proto__在实例对象(new Star)中,指向原型prototype。

11.4 原型继承

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

的特性。

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

<body>
  <script>
    function Person(){
        this.eyes = 2
        this.head = 1
    }
    function Woman(){
        
    }
     function Man(){
         
     }
     //Woman通过原型来继承Person 
     Woman.prototype = new Person()
     //指会原来的构造函数
     Woman.prototype.constructor = Woman
     //给女人添加一个生孩子的方法
     Woman.prototype.baby = function (){
         console.log('宝贝')
     }
      const agenle = new Woman()
  </script>

11.5 原型链

只要是原型对象就有constructor。只要是对象,就有对象原型(proto),指向构造函数的原型对象(prototype)构造函数的原型对象是一个对象,就有对象原型(__proto__),指向Object原型对象(Object.prototype),Object原型对象中含有__proto__,指向null。

JavaScript高级学习-LMLPHP

JavaScript高级学习-LMLPHP

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

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

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

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

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

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

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

12、深浅拷贝

拷贝内容是基本数据类型,深拷贝(堆)。内容是引用数据类型,浅拷贝(栈)。

12.1 浅拷贝

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

浅拷贝:拷贝的是地址

常见方法:

  1. 拷贝对象:Object.assgin() / 展开运算符 {...obj} 拷贝对象
  2. 拷贝数组:Array.prototype.concat() 或者 [...arr]
const obj = {
    uname:'zf',
    age:22
}
const o = {...obj}


const b = {}
Object.assign(o,obj)
  • 直接赋值是直接拷贝对象栈里面的地址
  • 浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还是会影响
  • 拷贝对象之后,里面的属性是简单数据类型直接拷贝值
  • 引用数据类型拷贝地址

12.2 深拷贝

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

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

常见方法:

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

1、递归实现深拷贝

函数递归:

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

  • 简单理解:函数内部自己调用自己, 这个函数就是递归函数
  • 递归函数的作用和循环效果类似
  • 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
//浅拷贝的递归
const obj ={
      uname:'阿凡',
      age:18
    }
    const o = {}
    // 拷贝函数
    function deepCopy(newObj,oldObj) {
      // 遍历对象
      for(let k in oldObj){
        // k 属性名  oldObj[k] 属性值
        //newObj[k] === o.uname
        newObj[k] = oldObj[k]
      }
    }
    deepCopy(o,obj)  //函数调用 两个参数
    console.log(o);
const obj ={
      uname:'阿凡',
      age:18,
      hobby:['唱歌','跳舞','rap']
    }
    const o = {}
    // 拷贝函数
    function deepCopy(newObj,oldObj) {
      // 遍历对象
      for(let k in oldObj){
        // k 属性名  oldObj[k] 属性值
        //newObj[k] === o.uname
        // 处理数组问题
        if(oldObj[k] instanceof Array){
          newObj[k] =[]
          deepCopy(newObj[k],oldObj[K])
        }else if(oldObj[k] instanceof Object){  //处理对象问题
          newObj[k] ={}
          deepCopy(newObj[k],oldObj[K])
        }
        else{
          newObj[k] = oldObj[K]
        }
      }
    }

总结:

用到函数递归

普通拷贝,直接赋值。遇到数组,需要再次调用递归函数。遇到对象,再次调用递归函数。先判断数组,后判断对象。

2、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>

3、JSON序列化

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

13、异常处理

throw

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

总结:

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 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

<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 代码段,并截获到错误信息

debugger

相当于断点调试

14、处理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>

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

箭头函数

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

  • 箭头函数默认帮我们绑定外层this的值,所以箭头函数this的值和外层的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事件回调函数不推荐使用箭头函数,如下代码所示:

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

JavaScript高级学习-LMLPHP

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

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

改变this指向

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

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

总结:

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

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

总结:

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

bind

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

<script>
  // 普通函数
  function sayHi() {
    console.log(this)
  }
  let user = {
    name: '小明',
    age: 18
  }
  // 调用 bind 指定 this 的值 返回值是一个函数,但是这个函数中的this是更改过的
  let sayHello = sayHi.bind(user);
  // 调用使用 bind 创建的新函数
  sayHello()
</script>

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

15、防抖节流

15.1 防抖

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

使用场景:搜索框输入,手机号,邮箱验证输入监测。

实现方式:

lodash提供的防抖来处理

_.debounce(func,[wait=0],[option=])
const box = document.querySelector('.box')
let i = 1
function mouseMove(){
    box.innerHTML = i++
}
box.addEventListener('mousemove',_.debounce(mouseMove,500))

手写防抖函数:防抖核心是定时器

  1. 声明一个定时器变量
  2. 当鼠标每次滑动都先判断是否有定时器,如果有定时器先清除以前的定时器
  3. 如果没有定时器则开启定时器,记得存到变量里面
  4. 在定时器里面调用要执行的函数
//1. 声明一个定时器变量

//2. 当鼠标每次滑动都先判断是否有定时器,如果有定时器先清除以前的定时器

//3. 如果没有定时器则开启定时器,记得存到变量里面

//4. 在定时器里面调用要执行的函数
function debounce(fn,t){
    let timer
    //return 返回一个匿名函数
    return function(){
        if(timer) clearTimeout(timer)
       timer = setTimeout(function(){
            fn()//加小括号调用fn函数
        },t)
    }
}
box.addEventListener('mousemove',_.debounce(mouseMove,500))

15.2 节流

节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。

运用场景:鼠标移动(mousemove)页面尺寸缩放resize 滚动条滚动(scroll)

实现方式:

1、lodash提供的节流函数来处理

const box = document.querySelector('.box')
let i = 1
function mouseMove(){
    box.innerHTML = i++
}
box.addEventListener('mousemove',_.throttle(mouseMove,500))

2、手写一个节流函数来处理

function throttle(fn,t){
    let timer = null
    //return 返回一个匿名函数
    return function(){
        if(!timer){
            timer = setTimeout(function(){
            fn()//加小括号调用fn函数
            timer = null
        },t)
        }
    }
}
06-30 11:15