[TOC]

了解函数中 this 在不同场景下的默认值,动态指定函数 this 的值,提升代码封装的灵活度。

  • 能够区分不同场景下函数中 this 的默认值
  • 知道箭头函数的普通函数的区别,掌握箭头函数的使用
  • 能够动指定函数中 this 的值
  • 了解基于类的面向对象的实现语法

    一、this

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

  1. 事件处理函数中,this表示事件源(事件给谁添加的,this就表示谁)
  2. 构造函数中或者类中this、对象的方法中,this表示实例对象
  3. 普通函数中、全局中、回调函数中,this表示window对象
  4. 箭头函数中没有 this(如果出现this,则按照作用域链去查找)

课堂代码

// 1. 事件处理函数中,this表示事件源(事件给谁添加的,this就表示谁)
// let btn = document.querySelector('button')
// btn.addEventListener('click', function () {
//   // console.log(this) // btn 
//   console.log(this === btn) // true,说明this就是btn按钮
// })




// 2. 构造函数中或者类中this、对象的方法中,this表示实例对象
// let obj = {
//   age: 50,
//   say: function () {
//     // console.log(this) // obj对象,方法中的this表示当前的对象
//     console.log(this.age)
//   }
// }
// // console.log(obj.age)
// obj.say()

// function Person() {
//   // this 表示根据这个构造函数实例化出来的对象
//   this.age = 100
//   this.say = function () {
//     console.log(this.age) // 实例对象的方法中的this,还是表示实例对象 
//   }
// }
// let obj = new Person()

// class Person {
//   constructor() {
//     this.age = 200
//     this.say()
//   }
//   say() {
//     console.log(this.age)
//   }
// }
// let obj = new Person()

// 3. 普通函数中、全局中、回调函数中,this表示window对象
console.log(this)

function abc() {
  console.log(this)
}
abc()

setTimeout(function () {
  console.log(this) // window
})

// 4. 箭头函数中没有this

1.1 默认值

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

普通函数

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

<script>
  // 普通函数
  function sayHi() {
    console.log(this);
  }
  // 函数表达式
  let sayHello = function () {
    console.log(this);
  }

  // 函数的调用方式决定了 this 的值
  sayHi(); // window
  window.sayHi();

     // 普通对象
  let 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 变量。

<script>
  console.log(this); // 此处为 window
  // 箭头函数
  let sayHi = function() {
    console.log(this); // 该箭头函数中的 this 为函数声明环境中 this 一致
  }

  // 普通对象
  let 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 节点
  let 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); // widow
  }

  let p1 = new Person();
  p1.walk();
</script>

1.2 定义值

call、apply、bind只能由函数调用
以上归纳了普通函数和箭头函数中关于 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

</script>

总结:

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

基于call的使用:

<input type="checkbox">
<input type="checkbox">
<input type="checkbox">
<button>判断</button>

<script>
  // 扩展一个小知识(记一下规律)
  let btn = document.querySelector('button')
  let ck = document.querySelectorAll('input')
  btn.addEventListener('click', function () {
    // console.log(ck) // 伪数组
    // 复选框.checked
    // console.log(ck[0].checked) // 如果复选框选中,则返回true;否则返回false
    // console.log(ck[1].checked)
    // console.log(ck[2].checked)
    // 判断三个复选框是否都选中(checked属性值都是true)了?
    // let result = [6, 9, 7, 8].every(item => item > 5)
    // console.log(result)

    let result = [].every.call(ck, item => item.checked === true)

    // 规律是:如果一个伪数组,希望使用数组方法的话,可以使用下面的语法
    // [].方法.call(伪数组, 方法的参数)

    // let result = ck.every(item => {
    //   return item.checked === true
    // })
    console.log(result)
  })
</script>

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

课堂代码:

// function sayHi(x, y) {
//   console.log(x + y)
//   console.log(this)
// }

// let user = { name: 'user', age: 20 }
// let student = { name: 'stu', age: 30 }

// // 
// // 函数.apply(对象)     // 调用函数,并且把函数中this改为传入的那个对象
// // sayHi.apply(user)
// // sayHi.apply(student)
// // sayHi.call(user, 3, 5)
// sayHi.apply(user, [3, 5])


// apply的应用
// Math.max(3, 2, 9, 0, 7)
console.log(Math.max.apply(null, [3, 2, 9, 0, 7]))
console.log(Math.max(...[5, 10, 20, 15]))

总结:

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

    bind

    bind 方法并不会调用函数,而是创建一个指定了 this 值的新函数,使用方法如下代码所示: ```javascript function sayHi(x, y) { console.log(x + y) console.log(this) }

let user = { name: ‘张三’, age: 20 }

// 语法: // call ,表示调用函数,并修改函数中this的指向 // bind,得到一个新函数,并修改函数中this的指向 // 函数.bind(对象, 3, 5) // let fn = sayHi.bind(user, 3, 5) // fn()

// sayHi.bind(user, 3, 5)()

let fn = sayHi.bind(user) fn(6, 5)

注:`bind` 方法创建新的函数,与原函数的唯一的变化是改变了 `this` 的值。<br />**改变this三个方法总结:**

call: fun.call(this,arg1, arg2,……)

apply: fun.apply(this, [arg1, arg2,……])

bind: fun.bind(this, arg1, arg2,……)

相同点: 都可以用来改变this指向,第一个参数都是this指向的对象 区别: call和apply:都会使函数执行,但是参数不同 bind:不会使函数执行,参数同call

<a name="Lm3gC"></a>
# 二、class
> 了解 JavaScript 中基于 class 语法的面向对象编程,为后续课程中的应用做好铺垫。

传统面向对象的编程序语言都是【类】的概念,对象都是由类创建出来,然而早期 JavaScript 中是没有类的,面向对象大多都是基于构造函数和原型实现的,但是 ECMAScript 6  规范开始增加了【类】相关的语法,使得 JavaScript 中的面向对象实现方式更加标准。
<a name="GKBPj"></a>
## 2.1 封装
class(类)是 ECMAScript 6 中新增的关键字,专门用于创建类的,类可被用于实现逻辑的封装。
```html
<script>
  // 创建类
  class Person {
    // 此处编写封装逻辑
  }

  // 实例化
  let p1 = new Person();
  console.log(p1);
</script>

实例成员

<script>
  // 创建类
  class Person {
    // 实例属性
    name = '小明';

    // 实例方法
    sleep () {
      console.log('sleeping...')
    }
  }

  // 实例化
  let p1 = new Person();
  p1.sayHi();
</script>

总结:

  • 关键字 class 封装了所有的实例属性和方法
  • 类中封装的并不是变量和函数,因此不能使用关键字 letconstvar

    静态成员

    <script>
    // 创建类
    class Person {
     // 静态属性
     static version = '1.0.0';
    
     // 静态方法
     static getVersion = function () {
       console.log(this.version);
     }
    }
    
    // 静态方法直接访问
    console.log(Person.version);
    Person.getVersion();
    </script>
    

    总结:

  • static 关键字用于声明静态属性和方法

  • 静态属性和方法直接通过类名进行访问

    构造函数

    创建类时在类的内部有一个特定的方法 constructor ,该方法会在类被实例化时自动被调用,常被用于处理一些初始化的操作。

    <script>
    class Person {
      // 实例化时 立即执行
      // 构造函数、构造方法、构造器
      constructor (name, age) {
        this.name = name;
        this.age = age;
      }
      // 实例方法
      walk () {
        console.log(this.name + '正在走路...');
      }
    }
    
    // 实例化
    let p1 = new Person('小明', 18);
    p1.walk();
    </script>
    

    总结:

  • constructor 是类中固定的方法名

  • constructor 方法在实例化时立即执行
  • constructor 方法接收实例化时传入的参数
  • constructor 并非是类中必须要存在的方法

    2.2 继承

    extends

    extends 是 ECMAScript 6 中实现继承的简洁语法,代码如下所示:

    <script>
    class Person {
      // 父类的属性
      legs = 2;
      arms = 2;
      eyes = 2;
          // 父类的方法
      walk () {
        console.log('人类都会走路...');
      }
          // 父类的方法
      sleep () {
        console.log('人都得要睡觉...');
      }
    }
    
    // Chinese 继承了 Person 的所有特征
    class Chinese extends Person {}
    
    // 实例化
    let c1 = new Chinese();
    c1.walk();
    </script>
    

    如上代码所示 extends 是专门用于实现继承的语法关键字,Person 称为父类、Chinese 称为子类。

    super

    在继承的过程中子类中 constructor 中必须调 super 函数,否则会有语法错误,如下代码所示: ```html

    总结:
    
    1. 子类如果不存在 constructor 则可以不调用 super()
    1. 子类存在 constructor 则必须调用 super()
    1. 在子类方法中,还能通过 `super.xxx()` 的方式调用父类方法
    <a name="CkeiI"></a>
    ## 2.3 写在最后
    ECMAScript 6 中基于类的面向对象相较于构造函数和原型对象的面向对象本质上是一样的,基于类的语法更为简洁,未来的 JavaScript 中也都会是基于类的语法实现,当前阶段先熟悉基于类的语法,后面课程中会加强基于类语法的实践。
    <a name="CV0TE"></a>
    # 三、对象的拷贝
    拷贝不是直接赋值
    <a name="lkf7M"></a>
    ## 3.0 instanceof
    
    - instanceof 用于判断一个对象的构造函数是什么?
    - 语法:`对象 instanceof 构造函数`
    - 对象 instanceof 原型链上的任何一个构造函数,都会得到true
    ```javascript
    // instanceof是一个关键字,用于检测对象的构造函数是什么
    function Person() {
    
    }
    let obj = new Person()
    
    // console.log(对象 instanceof 构造函数)
    console.log(obj instanceof Person) // true
    console.log(obj instanceof Array) // false
    console.log(obj instanceof Object) //true。因为 对象 instanceof 原型链上的任何一个构造函数,都会得到true
    
    
    // 如何判断数组和对象
    
    // let arr = [3, 4, 5]  // new Array(3, 4, 5)
    // let obj = { name: 'zs' }
    
    // if (obj instanceof Object) {
    //   // 说明是对象
    //   console.log('obj是对象')
    // }
    
    // if (arr instanceof Array) {
    //   console.log('arr是数组')
    // }
    
    // console.log(Array.isArray(arr)) // true
    // console.log(Array.isArray(obj)) // false
    

    3.0 递归函数

    // 递归入门
    // let i = 1
    // function fn() {
    //   // 加一个条件,在适当的时候,退出函数,否则就形成死循环了
    //   if (i > 10) return
    //   console.log(i)
    //   i++
    //   fn()
    // }
    
    // fn()
    
    // -------------- 计算阶乘 ------------------------------
    // 10的阶乘 = 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1
    // 5的阶乘 = 5 * 4 * 3 * 2 * 1
    // 规律:   10的阶乘 = 10 * 9的阶乘
    
    // 写递归函数,一定要相信自己。我写的函数一定会完成这个功能
    // 接下来,表演一下,我写一个函数,功能是:   计算一个数的阶乘
    // 当我需要计算一个数的阶乘的时候,就调用这个函数
    // function fn(n) {
    //   // return n的阶乘
    //   if (n === 1) {  // 如果n是1,则直接返回1
    //     return 1
    //   }
    //   // 如果n不是1,自己计算
    //   return n * fn(n - 1)
    // }
    // console.log(fn(10))
    
    
    // --------------------------------------------------
    // 计算斐波那契数列
    // 位置: 1   2   3   4   5   6   7   8  ........................... 40
    // 数字: 1   1   2   3   5   8   13  21 ........................... ?
    
    // 规律:位置n的数字 = n-1位置的数字 + n-2位置的数字
    
    // 写一个函数,函数的功能:计算一个位置的数字
    function fn(n) {
      if (n == 1 || n == 2) {
        return 1
      }
      // return n-1位置的数字 + n-2位置的数字
      return fn(n - 1) + fn(n - 2)
    }
    console.log(fn(40))
    

    3.1 浅拷贝

    let obj1 = {
      name: 'zs',
      age: 20,
      height: 180
    }
    
    let obj2 = {}
    
    // 循环遍历 obj1,循环一次,将一个属性拷贝到 obj2 里面
    for (let key in obj1) {
      // key 表示对象的属性
      // console.log(key) // name  age  height
      // console.log(obj1[key]) // zs  20   180
      obj2[key] = obj1[key]
    }
    
    obj2.height = 200
    console.log(obj1)
    console.log(obj2)
    

    3.2 深拷贝

    // 深拷贝:不止拷贝对象的一层,拷贝的时候,如果发现对象的某个属性是引用类型的,则继续拷贝;直至得到两个完全独立的对象
    let obj1 = {
      name: 'zs',
      age: 20,
      info: [180, 80],
      dog: {
        color: '#000',
        age: 3
      }
    }
    
    let obj2 = {}
    
    // 递归
    // 写一个函数,功能是实现浅拷贝(拷贝的时候,判断一下,如果对象的值是数组或者对象,则继续浅拷贝)
    function kaobei(obj1, obj2) {
      for (let key in obj1) {
        // 'obj1[key] 是数组吗?'
        if (Array.isArray(obj1[key])) {
          obj2[key] = []
          kaobei(obj1[key], obj2[key])
        } else if (obj1[key] instanceof Object) {  // 'obj1[key] 是对象吗?'
          obj2[key] = {}
          kaobei(obj1[key], obj2[key])
        } else {
          obj2[key] = obj1[key]
        }
      }
    }
    
    kaobei(obj1, obj2)
    
    // 改其中一个对象
    obj1.info[0] = 170
    obj1.dog.color = 'red'
    
    console.log(obj1)
    console.log(obj2)