JavaScript 中的 this

在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被 调用时 this 的值也可能会不同。

确定 this 的值

在非严格模式下,总是指向一个对象。但是在严格模式下,可以指定任意值。

开启严格模式的两种方法:

  1. 在整个脚本顶部开启
  2. 在函数顶部开启
1
2
3
4
5
6
// 为全局开启严格模式
'use strict'
function func() {
// 为局部开启严格模式
'use strict'
}

然后就可以根据不同的模式来确认this指向啦,

  1. 全局执行环境中,指向全局对象(非严格模式、严格模式)
  2. 函数内部,取决于函数被调用的方式
    1. 直接调用的this值:
      1. 非严格模式:全局对象(window)
      2. 严格模式:undefined
    2. 对象方法调用的this值:
      1. 调用者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1.全局执行环境
// 非严格模式: 不做任何设置,直接写就是非严格模式
console.log(this) // window
// 严格模式: 代码顶部加上 'use strict' 即可
'use strict' // 为整个脚本开启严格模式
console.log(this) // window

// 2.函数内部
// 2.1 直接调用-非严格模式
function func() {
console.log(this) // 全局对象window
}
func()

// 2.1 直接调用-严格模式
function func() {
'use strict'
console.log(this) // undefined
}
func()

// 2.2 对象方法调用
const fans = {
name: 'ikun',
sing() {
'use strict'
console.log(this)
}
}
fans.sing() // fans对象

改变 this 的值

指定 this 的方法有两类

  1. 调用函数传入具体的 this:
    1. call(参数1, 其他参数)
      1. 参数1:this 值
      2. 其他参数:传递给函数的其他参数,可以有很多个
    2. apply(参数1, [参数2])
      1. 参数1:this 值
      2. 参数2:以数组形式给函数传递参数
  2. 创建绑定 this 的函数:
    1. bind()
      1. 和 call 类似,但是不执行函数
      2. 会返回一个绑定了 this 值的新函数
    2. 箭头函数:最近的 this 值是哪个,就是哪个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function func(p1, p2) {
console.log(this)
console.log(p1, p2)
}
const person = {
name: 'Alien'
}
// call调用函数
func.call(person, 1, 2) // 打印person对象, 1, 2
// apply调用函数
func.apply(person, [3, 4]) // 打印person对象, 3, 4
// 创建绑定this的函数 bind
const bindFunc = func.bind(person, 123) // 不会直接执行,而是创建新函数
bindFunc(456) // 打印person对象, 123, 456 上面已经传入一个参数,这个函数会跟在后面继续传递
// 箭头函数
const student = {
name: 'ikun',
sayThis: function() {
console.log(this)
}
sayThat: () => {
console.log(this)
}
}
student.sayThis() // 打印student对象,对象方法,谁调用指向谁
student.sayThat() // 打印window对象,箭头函数,最近的this指向谁,它就指向谁

重写 call() 方法

实现了一个自定义的 myCall 方法,能够在任意函数上调用,并且可以指定函数内部 this 的指向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在function的原型上创建myCall方法,保证所有函数都可以调用
Function.prototype.myCall = function(thisArg, ...args) { // 用剩余函数接受剩余的参数
const f = Symbol() // 使用 Symbol 作为属性名可以确保在对象上不会与对象原有的属性冲突
thisArg[f] = this  // 按照谁调用就指定谁的原则
const res = thisArg[f](...args)    // 调用添加的函数,并用展开运算符传入参数
delete thisArg[f]  // 把新建的方法从对象中删除
return res  // 返回参数
}

const person = {
name: 'Alien'
}
function func(num1, num2) {
console.log(this)
console.log(num1, num2)
return num1 + num2
}
const res = func.myCall(person, 5, 7)
console.log(res);
  • Function原型对象上添加 myCall() 方法,保证所有函数都可以调用
  • 方法内部通过动态为对象添加方法来指定 this 的指向
  • 调用完毕后再删除 delete() 方法
  • Symbol函数的目的是避免重名导致的错误,每次 symbol 的返回值都是唯一的
  • thisArg[f] 使用了 Symbol 作为属性名,正常情况下不会影响原函数或覆盖,而 thisArg.f 直接使用了字符串作为属性名,遇到同名方法可能会覆盖

重写 apply() 方法

实现自定义函数 myApply() ,与 myCall 方法类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在function的原型上创建myApply方法,保证所有函数都可以调用
Function.prototype.myApply = function (thisArg, args) { // 用剩余函数接受剩余的参数
const f = Symbol()
thisArg[f] = this  // 按照谁调用就指定谁的原则
const res = thisArg[f](...args)    // 调用添加的函数,并用展开运算符传入参数
delete thisArg[f]  // 把新建的方法从对象中删除
return res  // 返回参数
}
const person = {
name: 'Alien'
}
function func(num1, num2) {
console.log(this)
console.log(num1, num2)
return num1 + num2
}
const res = func.myApply(person, [5, 7])
console.log(res);
  • myCall() 的写法一致,只需要将传进来的参数定为数组即可

重写 bind() 方法

实现自定义函数 myBind() ,需要实现新函数参数的拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在function的原型上创建myBind方法,保证所有函数都可以调用
Function.prototype.myBind = function (thisArg, ...args) { // 用剩余函数接受剩余的参数
return (...args2) => {
// thisArg 需要指定的this
// args 调用myBind时传入的参数
// args2 调用新函数时传入的参数
return this.myCall(thisArg, ...args, ...args2)
}
}
const person = {
name: 'Alien'
}
function func(num1, num2, num3, num4) {
console.log(this)
console.log(num1, num2, num3, num4)
return num1 + num2 + num3 + num4
}
const newBind = func.myBind(person, 5, 7)
const res = newBind(1, 4)
console.log(res);
  1. function原型上添加myBind函数,参数1为绑定的this,参数2为绑定的参数
  2. 内部返回一个新箭头函数,目的是绑定作用域中的this
  3. 返回的函数内部,通过call进行this和参数绑定
  4. 通过call的参数2和参数3指定绑定的参数,和调用时传递的参数

JS 继承

ES5-原型链继承

  1. 将父类的实例作为子类的原型实现继承
  2. 这种继承方法的缺点是父类中的引用类型数据会被所有子类共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 父类
function Parent(name) {
this.name = name
this.foods = ['西蓝花', '西红柿']
this.sayFoods = function () {
console.log(this.foods)
}
}
// 子类
function Son() {
}
// 将父类的实例 作为子类的原型
Son.prototype = new Parent('jack')
const s1 = new Son()
s1.sayFoods()// ['西蓝花', '西红柿']

const s2 = new Son()
s2.sayFoods() // ['西蓝花', '西红柿']

s2.foods.push('西葫芦')

s2.sayFoods()// ['西蓝花', '西红柿', '西葫芦']
s1.sayFoods()// ['西蓝花', '西红柿', '西葫芦']

ES5-构造函数继承

  1. 在子类的构造函数中通过call或者apply调用父类的构造函数
  2. 这种继承方法的缺点是:子类没法使用父类原型上的属性/方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 父类
function Parent(name) {
this.name = name
}
Parent.prototype.sayHi = function () {
console.log('你好,我叫:', this.name)
}

// 子类
function Son(name) {
Parent.call(this, name)
}

const s1 = new Son('lucy')
const s2 = new Son('rose')
s1.sayHi() // 报错

ES5-组合继承

  1. 组合继承的核心步骤有2步:
    1. 通过原型链继承公共的属性和方法
    2. 通过构造函数继承实例独有的属性和方法
  2. 组合继承的特点:调用2次父类的构造函数,浪费性能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 父类
function Person(name) {
this.name = name
}
// 方法加父类原型上
Person.prototype.sayHi = function () {
console.log(`你好,我叫${this.name}`)
}
// 子类构造函数
function Student(name, age) {
// 调用父类构造函数传入this
Person.call(this, name)
// 子类独有的属性和方法单独设置
this.age = age
}
// 设置子类的原型为 父类实例
Student.prototype = new Person()
// 调用子类的构造函数
const s = new Student('李雷', 18)
// 可以使用原型链上的 属性和方法 也可以使用 通过构造函数获取的父类的属性和方法

ES5-原型式继承

  1. 原型式继承的核心步骤是:对某个对象进行浅拷贝,可以通过内置apiObject.create()实现,不需要调用构造函数即可实现继承,主要针对于继承对象的情况
  2. 原型式继承的缺点是:父类中的引用数据类型,会被所有子类共享
1
2
3
4
5
6
7
8
9
10
11
12
const parent = {
name: 'parent',
age: 25,
friend: ['rose', 'ice', 'robot'],
sayHi() {
console.log(this.name, this.age)
}
}
const son1 = Object.create(parent)
const son2 = Object.create(parent)
son1.friend.push('lucy')
console.log(son2.friend)

ES5-寄生式继承

  1. 寄生式继承的核心步骤是:基于对象创建新对象(可以使用Object.create),并且为新创建的对象增加新的属性和方法
  2. 寄生式继承和上一节学习的原型式继承的区别是:创建出来的新对象,会额外的增加新的属性/方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createAnother(origin) {
// Object.create基于原型创建新对象,对属性进行浅拷贝
const clone = Object.create(origin)
// 为对象增加属性/方法
clone.sayHi = function () {
console.log('你好')
}
return clone
}
const parent = {
name: 'parent',
foods: ['西蓝花', '炒蛋', '花菜']
}
const son1 = createAnother(parent)
const son2 = createAnother(parent)

ES5-寄生组合式继承

  1. 寄生组合式继承的核心步骤是:通过构造函数来继承属性,通过原型链来继承方法
  2. 寄生组合式继承和组合式继承的区别是:原型链的继承并没有调用父类的构造函数,而是直接基于父类的原型创建一个新副本实现继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 继承原型函数
function inheritPrototype(son, parent){
const prototype = Object.create(parent.prototype, {
constructor: {
value: son
}
})
son.prototype = prototype
}

// 父类
function Parent(name) {
this.name = name
this.foods = ['西蓝花', '西葫芦', '西红柿']
}

Parent.prototype.sayHi = function () {
console.log(this.name, `我喜欢吃,${this.foods}`)
}

// 子类借用父类的构造函数
function Son(name, age) {
Parent.call(this, name)
this.age = age
}
// 完成原型继承
inheritPrototype(Son,Parent)
// 可以继续在原型上添加属性/方法
Son.prototype.sayAge = function () {
console.log('我的年龄是', this.age)
}
const son1 = new Son('jack', 18)
const son2 = new Son('rose', 16)

ES6-class实现继承

class 核心语法:

  1. 如何定义及使用类:
  2. 如何定义实例属性/方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义类
class Person {
// 实例属性,方便一眼确认有哪些
name
food
// 构造方法,类似于构造函数,new的时候会调用,内部的this就是实例化的对象
constructor(name, food) {
this.name = name
this.food = food
}
// 实例方法
sayHi() {
console.log(`你好,我叫${this.name},我喜欢吃${this.food}`)
}
}
const p = new Person('小黑', '西蓝花')
p.sayHi()
  1. 通过class 类名{}的形式来定义类
  2. 内部直接写实例属性,可以设置默认值
  3. 实例方法的添加方式为方法名(){}
  4. 构造函数通过constructor进行添加
  5. 通过new 类名()创建实例,会调用构造函数constructor

class 实现继承

  1. 子类通过extends继承继承父类
  2. 子类如果需要重新定义构造函数,必须在内部通过super关键字调用父类的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student extends Person {
song
constructor(name, food, song) {
// 子类构造函数使用this以前必须调用super
super(name, food)
this.song = song
}
// 添加方法
sing() {
console.log(`我叫${this.name},我喜欢唱${this.song}`)
}
}
const s = new Student('李雷', '花菜', '孤勇者')
s.sayHi()
s.sing()

class 私有、静态属性和方法

  1. class中私有属性/方法
    1. 定义和使用时需要使用关键字#
    2. 私有属性只能在类的内部使用,外部无法使用(代码中)
    3. Chrome的控制台中为了方便调试,可以直接访问
  2. class中静态属性/方法
    1. 定义和使用时需要使用关键字static
    2. 通过类访问
    3. 静态方法中的this是类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person {
constructor(name) {
this.name = name
}
// 通过#作为前缀添加的属性会变为私有
// 私有属性
#secret = '我有一个小秘密,就不告诉你'
// 私有方法
#say() {
// 私有属性可以在
console.log('私有的say方法')
}
info() {
// 在类的内部可以访问私有属性调用私有方法
console.log(this.#secret)
this.#say()
}
// 通过 static定义静态属性/方法
static staticMethod() {
console.log('这是一个静态方法')
console.log(this)
}
static info = '直立行走,双手双脚'
}
const p = new Person('jack')
console.log(p)
// 外部无法访问 点语法访问直接报错,通过[]无法动态获取
console.log(p['#secret'])
p.info()
// 通过类访问静态属性/方法
Person.staticMethod()
console.log(Person.info)

fetch

AJAX & axios & fetch的关系:

AJAXAJAX 是一种基于原生 JavaScript 的异步请求技术。它使用 XMLHttpRequest 对象来发送请求和接收响应。

axiosaxios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 中使用。它提供了更高级别的封装,使发送请求和处理响应更加简单和灵活。

fetchfetch 是浏览器内置的 API,用于发送网络请求。它提供了一种现代化、基于 Promise 的方式来进行网络通信。用法和axios类似,但相比于 axios,它的功能和封装级别更为简单。

fetch 核心语法

  1. fetch函数的参数:
    1. 参数1:请求的url地址
    2. 参数2:以对象的形式设置请求相关的内容比如,方法,请求头,提交的数据等。
  2. fetch获取到响应结果,需要如何解析:
1
2
3
4
fetch(资源地址,{...配置项对象})
.then(response=>{
// 接收请求
})

GET请求

  1. fetch发送get请求时,不需要设置请求方法,因为默认的就是get
  2. URLSearchParams可以用来创建或者解析查询字符串,这里通过它将对象转为查询字符串
1
2
3
4
5
6
7
8
9
document.querySelector('#btn').addEventListener('click', async () => {
const p = new URLSearchParams({
pname: '广东省',
cname: '广州市'
})
const res = await fetch('http://hmajax.itheima.net/api/area?' + p.toString())
const data = await res.json()
console.log(data);
})

POST请求-提交JSON

  1. fetch函数的第二个参数可以设置请求头,请求方法,请求体,根据接口文档设置对应的内容即可
  2. 可以通过JSON.stringify将对象转为JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
document.querySelector('#btn').addEventListener('click', async function () {
// 通过headers设置请求头
const headers = new Headers()
// 通过 content-type指定请求体数据格式
headers.append('content-type', 'application/json')
// 参数1 url
// 参数2 请求配置
const res = await fetch('http://hmajax.itheima.net/api/register', {
method: 'post',// 请求方法
headers, // 请求头
// 请求体
body: JSON.stringify({
username: 'itheima9876',
password: '123456'
})
})
const json = await res.json()
console.log(json)
})

POST请求-提交FormData

  1. fetch提交FormData时不需要额外的设置请求头
  2. 实例化FormData之后可以通过append(key,value)方法来动态添加数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<input type="file" class="file" accept="image/*">
<script>
document.querySelector('.file').addEventListener('change', async function (e) {
// 生成FormData对象并添加数据
const data = new FormData()
data.append('img', this.files[0])
const res = await fetch('http://hmajax.itheima.net/api/uploadimg', {
method: 'post',
body: data
})
const json = await res.json()
console.log(json)
})
</script>

Generator

Generator对象由生成器函数返回并且它符合可迭代协议迭代器协议。他可以用来控制流程,语法行为和之前学习的函数不一样

  1. 可以通过生成器函数(function* xxx(){})来生成Generator对象:
  2. 通过Generator对象的next方法可以获取yield表达式之后的结果
  3. 通过for of获取每一个yield的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 通过function* 创建生成器函数 
function* foo() {
// 遇到yield表达式时会暂停后续的操作
yield 'a'
yield 'b'
yield 'c'
return 'd'
}
// 2. 调用函数获取生成器
const f = foo()
// 3. 通过next方法获取yield 之后的表达式结果,会被包装到一个对象中
// 执行一次next 即可获取一次 yield之后的表达式结果
const res1 = f.next()
console.log(res1)// {value: 'a', done: false}
const res2 = f.next()
console.log(res2)// {value: 'b', done: false}
const res3 = f.next()
console.log(res3)// {value: 'c', done: false}
// 最后一次可以拿到return的结果
const res4 = f.next()
console.log(res4)// {value: 'd', done: true}
// done 为true之后,获取到的value为undefined
const res5 = f.next()
console.log(res5)// {value: undefined, done: true}
// 4. 通过for of 获取每一个yield之后的值,
const f2 = foo()
for (const iterator of f2) {
console.log(iterator)
} // "abc"

generator 实现id生成器

  1. 定义生成器函数
  2. 内部使用循环,通过yield返回id并累加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 通过function* 创建生成器函数 
function* generator() {
let id = 0
// 无限循环
while (true) {
// id累加并返回
yield id++
}
}
// 2. 调用函数获取生成器
const idMaker = generator()
// 3. 需要id的时候 通过next获取即可
const { value: id1 } = idMaker.next()
console.log(id1)
const { value: id2 } = idMaker.next()
console.log(id2)

generator-管理异步

  1. 使用Generator控制流程的本质是利用yield关键字来分隔逻辑比如示例中依次调用了多个接口,通过yield分隔,通过next来触发调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<button class="getWeather">天气查询</button>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.js"></script>
<script>
/**
* 需求:流程控制,依次查询,北上广深的天气预报
* 参考code: 北京 110100  上海 310100  广州 440100 深圳 440300
* 接口文档: https://apifox.com/apidoc/project-1937884/api-49760220
* */
function* weatherGenerator() {
// 北京
yield axios('http://hmajax.itheima.net/api/weather?city=110100')
// 上海
yield axios('http://hmajax.itheima.net/api/weather?city=310100')
// 广州
yield axios('http://hmajax.itheima.net/api/weather?city=440100')
// 深圳
yield axios('http://hmajax.itheima.net/api/weather?city=440300')
}

const cityWeather = weatherGenerator()
document.querySelector('.getWeather').addEventListener('click', async () => {
await cityWeather.next().value.then(res => {
console.log(res.data.data);
})
})
</script>

点击一次就获取一次,实现对异步的控制

image.png