3 - JavaScript进阶

1、作用域

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

1.1、局部作用域

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

1.1.1、函数作用域

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

<script>
	function getSum() {
        // 函数内部是函数作用域,属于局部变量
        const num = 10
    }
    console.log(num) // 此处报错,函数外部不能使用局部作用域变量
</script>

总结

  • 函数内部声明的变量,在函数外部无法被访问
  • 函数的参数也是函数内部的局部变量
  • 不同的函数内部声明的变量无法互相访问
  • 函数执行完毕后,函数内部的变量实际被清空了

1.1.2、块作用域

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

for (let i = 0; i < 5; i++) {
    // i 只能在该代码块中被访问
    console.log(i)  // 正常
}
// 超出了 i 的作用域
console.log(i)  // 报错

总结

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

1.2、全局作用域

<script>标签.js文件 的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
全局作用域中声明的变量,任何其他作用域都可以访问

// 全局作用域
// 全局作用域下声明了 num 变量
const num = 10
function fn() {
    // 函数内部可以使全局作用域的变量
    console.log(num)
}

注意

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

1.3、作用域链

思考代码:

let a = 1
function f() {
    let a = 2
    function g() {
        a = 3
        console.log(a)
    }
    g()
}
f()  // 3

作用域链的本质是底层的变量查找机制

  • 在函数被执行时,会优先查找当前函数作用域中查找变量
  • 如果当前作用域查找不到则会一次逐级查找父级作用域直到全局作用域

总结

  • 嵌套关系的作用域串联起来形成了作用域链
  • 相同作用域链中按着从小到大的规则查找变量
  • 子作用域能够访问父作用域,父级作用域无法访问子级作用域

1.4、垃圾回收机制

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

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

  • 内存的生命周期
    • JS 环境中的内存分配,一般有如下生命周期
      • 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
      • 内存使用:即读写内存,也就是使用变量、函数等
      • 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存
  • 说明
    • 全局变量一般不会回收(关闭页面回收)
    • 一般情况下局部变量的值,不用了会被自动回收
  • 内存泄漏:程序分配的内存由于某种原因程序未释放无法释放叫做内存泄漏

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

堆栈空间分配区别

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

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

引用计数法:

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

算法

  • 跟踪记录被引用的次数
  • 如果被引用了一次,那么就记录次数1,多次引用会累加++
  • 如果减少一个引用就减1 –
  • 如果引用次数是0,则释放内存
标记清除法:

现代浏览器已经不再使用引用计数法了。大多是基于标记清除算法的某些改进算法,总体思想都是一致的

核心

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

1.5、闭包

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

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

// 例子
function outer() {
    const a = 1
    function f() {
        console.log(a)
    }
    f()
}
outer()

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

闭包的基本格式

function outer() {
    let i = 0
    return function () {
        console.log(i)
    }
}
const fun = outer()
fun() // 1
// 外层函数使用内部函数的变量

闭包应用:实现数据的私有
比如,我们要做一个统计函数调用次数,函数调用一次,就++

// 使用闭包使得 数据(count) 不容易被修改,数据私有
function fn() {
	let count = 0
    return function() {
        count++
        console.log(`这是第${count}次调用函数`)
    }
}
const result = fu()
result()  // 1
result()  // 2

闭包可能会造成内存泄漏问题

1.6、变量提升

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

console.log(num)  // 输出 undefined
var num = 10

注意

  • 变量在未声明即被访问时会报语法错误
  • 变量在 var 声明之前即被访问,变量的值为 undefined
  • let / const 声明的变量不存在变量提升
  • 变量提升出现在相同作用域当中
  • 实际开发中推荐先声明再访问变量
  • 只提升变量声明,不提升变量赋值

说明

JS 初学者经常花很多时间才能习惯变量提升,还经常出现一些意想不到的bug,正因如此,ES6 引入了块级作用域,用 let 或者 const 声明变量,让代码写法更加规范和人性化。

2、函数进阶

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

2.1、函数提升

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

// 调用函数
foo()
// 声明函数
function foo() {
    console.log('声明之前即被调用...')
}
//-------------------------------------------
// 不存在提升现象
bar()  // 错误
var bar = function () {
    console.log('函数表达式不存在提升现象...')
}

总结

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

2.2、函数参数

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

2.2.1、动态参数

产品需求:写一个求和函数,不管用户传入几个参数,都要把和求出来

arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参

// 求和函数,计算所有参数的和
function sum() {
    // console.log(arguments)
    let s = 0
    for (let i = 0; i < arguments.length; i++) {
        s += arguments[i]
    }
    console.log(s)
}
// 调用求和函数
sum(2, 3)  // 两个参数
sum(1, 2, 3)  // 三个参数

总结

  • arguments 是一个伪数组,只存在于函数中
  • arguments 的作用是动态获取函数的实参
  • 可以通过 for 循环依次得到传递过来的实参

2.2.2、剩余参数

...自定义标识符 剩余参数允许我们将一个不定数量的参数表示为一个数组

function getSum(...arr) {
	// arr 得到 [1, 3, 4]
	console.log(arr)
}
getSum(1, 3, 4)
  1. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参
  2. 借助 ... 获取的剩余实参,是个真数组
function config(baseURL, ...other) {
	console.log(baseURL)  // 得到 'http://baidu.com'
	console.log(other)  // 得到 ['get', 'json']
}
// 调用函数
config('http://baidu.com', 'get', 'json')

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

2.2.3、展开运算符

展开运算符 ... 将一个数组进行展开

const arr = [1, 2, 3, 4, 5]
console.log(...arr)  // 1, 2, 3, 4, 5

说明:不会修改原数组

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

// 1. 求数组最大值
const arr1 = [1, 4, 8]
Math.max(...arr1)
// 2. 合并数组
const arr2 = [2, 3, 4]
const arr3 = [...arr1, ...arr2]

2.3、箭头函数(重要)

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

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

2.3.1、基本语法

语法1:基本写法

// 普通函数
const fn = function () {
    console.log('我是普通函数')
}
fn()
// 箭头函数
const fn = () => {
    console.log('我是普通函数')
}
fn()

语法2:只有一个参数时可以省略小括号

// 普通函数
const fn = function (x) {
    return x + x
}
console.log(fn())  // 2
// 箭头函数
const fn = x => {
    return x + x
}
console.log(fn())  // 2

语法3:如果函数体只有一行代码,可以写到一行上,并且无需写 return,直接返回值

// 普通函数
const fn = function (x, y) {
    return x + y
}
console.log(fn(1, 2))  // 3
// 箭头函数
const fn = (x, y) => x + y
console.log(fn(1, 2))  // 3

语法4:加括号的函数体返回对象字面量表达式

const fn1 = uname => ({ uname: uname })
console.log(fn1('pink老师'))

2.3.2、箭头函数参数

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

2.3.4、箭头函数 this

在箭头函数之前,每一个新函数根据它是被如何调用的来定义这个函数的 this 值,非常令人讨厌
箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层沿用 this

console.log(this)  // 此处为 window
const sayHi = function () {
    console.log(this)  // 普通函数指向调用者,此处为 window
}
btn.addEventListener('click', function () {
    console.log(this)  // 此处为 btn
})
const user = {
    name: '小米',
    // 该箭头函数中的 this 为函数声明环境中 this 一致
    walk: () => {
        console.log(this)  // 指向 window 不是 user
    }
}
user.walk()

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

const user = {
    name: '小米',
    sleep: function () {
        console.log(this)  // 指向 user
        const fn = () => {
            console.log(this)  // 指向 user
        }
        // 调用箭头函数
        fn()
    }
}
user.walk()

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

3、解构赋值

3.1、数组解构

const arr = [100, 200, 300]
const min = arr[0]
const avg = arr[1]
const max = arr[2]
console.log(min)  // 最小值
console.log(avg)  // 平均值
console.log(max)  // 最大值

以上要么不好记忆,要么书写麻烦,此时可以使用解构赋值的方法让代码更简洁

const [min, avg, max] = [100, 200, 300]
console.log(min)  // 最小值
console.log(avg)  // 平均值
console.log(max)  // 最大值

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

基本语法

  • 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
  • 变量的顺序对应数组单元值的位置依次进行赋值操作
// 普通的数组
const arr = [1, 2, 3]
// 批量声明变量 a b c
// 同时将数组单元值 1 2 3 依次赋值给变量 a b c
const [a, b, c] = arr
console.log(a)  // 1
console.log(b)  // 2
console.log(c)  // 3

典型应用交换2个变量

let a = 1
let b = 3;  // 注意:这里有分号
[b, a] = [a, b]
console.log(a)  // 3
console.log(b)  // 1
  1. 立即执行函数

    (function t() {})();
    // 或者
    ;(function t() {})()
    
  2. 数组解构

    // 数组开头的,特别是前面有语句的一定注意加分号
    ;[b, a] = [a, b]
    

1、变量多 单元值少的情况

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、变量少 单元值多的情况

3、利用剩余参数解决变量少 单元值多的情况

const [a, b, ...c] = [1, 2, 3, 4]
// c 为 [3, 4]  真数组

4、防止有 undefined 传递单元值的情况,可以设置默认值

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

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

5、按需导入赋值

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

6、支持多维数组的解构

const [a, b, [c, d]] = [1, 2, [3, ]]

3.2、对象解构

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

1、基本语法

  1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  2. 对象属性的值将被赋值给与属性名相同的变量
  3. 注意解构的变量名不要和外面的变量名冲突,否则报错
  4. 对象中找不到与变量名一致的属性时,变量值为 undefined
// 普通对象
const user = {
    name: '小明',
    age: 18
};
// 批量声明变量 name age
// 同时将数组单元值 小明 18 依次赋值给变量 name age
const { name, age } = user

console.log(name)  // 小明
console.log(age)  // 18

2、给新的变量名赋值

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

冒号表示 “什么值:赋值给谁

// 普通对象
const user = {
    name: '小明',
    age: 18
};
// 把 原来的name 变量重新命名为 uname
const { name: uname, age } = user
console.log(uname)  // 小明
console.log(age)  // 18

3、数组对象解构

const pig = [
    {
        name: '佩奇',
        age: 6
    }
]
const [{ name, age }] = pig
console.log(name, age)

4、多级对象解构

const people = [
    {
        name: '佩奇',
        family: {
            mother: '猪妈妈',
            father: '猪爸爸',
            sister: ‘乔治
        },
        age: 6
    }
]
const [{ name, family: { mother, father, sisiter } }] = people

5、在函数参数中解构

function render({ data }) {
	console.log(data)
}
render(msg)

3.3、遍历数组 forEach 方法(重点)

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

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

  • 语法

    被遍历的数组.forEach(function (当前数组元素, 当前元素索引) {
        // 函数体
    })
    
  • 例如

    const arr = ['pink', 'red', 'green']
    arr.forEach(function (item, index) {
        console.log(`当前数组元素是:${item}`)
        console.log(`当前数组元素的索引是:${index}`)
    })
    
  • 注意

    • forEach() 主要是遍历数组
    • 参数当前数组元素是必须要写的,参数索引可选

4、构造函数

4.1、深入对象

4.1.1、创建对象的三种方式

  • 利用对象的字面量创建对象

    const o = {
    	name: '佩奇'
    }
    
  • 利用 new Object 创建对象

    const o = new Object({
        name: '佩奇'
    })
    
  • 利用构造函数创建对象

4.1.2、构造函数

  • 构造函数:是一种特殊的函数,主要用来初始化对象
  • 使用场景:常规的 {…} 语法允许创建对象。比如我们创建了佩奇的对象,继续创建乔治的对象还需要重新写一遍,此时可以通过构造函数快速创建多个类似的对象
function Pig(name, age, gender) {
    this.name = name
    this.age = age
    this.gender = gender
}

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

  • 它们的命名以大写字母开头
  • 它们只能由 new 操作符来执行

说明

  • 使用 new 关键字调用函数的行为被称为实例化
  • 实例化构造函数时没有参数时可以省略 ()
  • 构造函数内部无需写 return,返回值即为新创建的对象
  • 构造函数内部的 return 返回值无效,所以不要写 return
  • new Object()new Date() 也是实例化构造函数

实例化执行过程

  • 创建新对象
  • 构造函数 this 指向新对象
  • 执行构造函数代码,修改 this,添加新的属性
  • 返回新对象

4.1.3、实例成员 & 静态成员

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

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

    说明

    • 为构造函数传入参数,创建解构相同但值不同的对象
    • 构造函数创建的实例对象彼此独立互不影响
  • 静态成员构造函数的属性和方法被称为静态成员(静态属性和静态方法)

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

    说明

    • 静态成员只能构造函数来访问
    • 静态方法中的 this 指向构造函数

    比如:Date.now()Math.PIMath.Random()

4.2、内置构造函数

在 JavaScript 中最主要的数据类型有 6 种:

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

引用数据类型:对象

但是,我们会发现有些特殊情况:

// 普通字符串
const str = 'andy'
console.log(str.length)

其实字符串、数值、布尔、等基本数据类型也都有专门的构造函数,这些我们称为包装类型。
JS 中几乎所有的数据都可以基于构造函数创建。

内置构造函数

  • 引用类型ObjectArrayRegExpDate
  • 包装类型StringNumberBoolean

4.2.1、Object

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

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

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

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

  • Object.keys()

    • 作用:获取对象中的所有属性名(键)

    • 语法

      const o = { name: '佩奇', age: 6 }
      // 获得对象所有的键,并且返回是一个数组
      const arr = Object.keys(o)
      console.log(arr)  // ['name', 'age']
      
    • 注意:返回的是一个数组

  • Object.values()

    • 作用:获取对象中的所有属性值(值)

    • 语法

      const o = { name: '佩奇', age: 6 }
      // 获得对象所有的值,并且返回是一个数组
      const arr = Object.values(o)
      console.log(arr)  // ['佩奇', 6]
      
    • 注意:返回的是一个数组

  • Object.assign()

    • 作用:常用于对象拷贝

    • 语法

      // 拷贝对象 把o拷贝给 obj
      const o = { name: '佩奇', age: 6 }
      const obj = {}
      Object.assign(obj, o)
      console.log(obj)  // { name: '佩奇', age: 6 }
      
    • 使用:经常使用的场景给对象添加属性

      // 给o新增属性
      const o = { name: '佩奇', age: 6 }
      Object.assign(o, { gender: '女' })
      console.log(o)  // { name: '佩奇', age: 6, gender: '女' }
      

4.2.2、Array

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

const arr = new Array(3, 5)
console.log(arr)  // [3, 5]

创建数组建议使用字面量创建,不用 Array 构造函数创建

数组常见实例方法-核心方法

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

    • 作用:返回累计处理的结果,经常用于求和等

    • 基本语法

      arr.reduce(function(){}, 起始值)
      arr.reduce(function(上一次值, 当前值){}, 初始值)
      
    • 参数

      • 如果有起始值,则把起始值累加到里面
    • 例子

      const arr = [1, 2, 3]
      // 没有起始值
      const total = arr.reduce(function(prev, current) {
          return prev + current
      })
      console.log(total)  // 6
      // 有起始值
      const total = arr.reduce(function(prev, current) {
          return prev + current
      }, 10)
      console.log(total)  // 16
      // 箭头函数写法
      const total = arr.reduce((prev, current) => return prev + current, 10)
      console.log(total)  // 16
      
    • reduce 执行过程

      • 如果没有起始值,则上一次值为数组的第一个数组元素的值
      • 每一次循环,把返回值给作为 下一次循环的上一次值
      • 如果有起始值,则 起始值作为上一次值
  • 实例方法 join:数组元素拼接为字符串,返回字符串(重点)

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

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

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

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

  • 实例方法 sort:对原数组单元值排序

  • 实例方法 splice:删除或替换原数组单元

  • 实例方法 reverse:反转数组

  • 实例方法 findIndex:查找元素的索引值

数组常见静态方法-伪数组转换为真数组

  • Array.from()

4.2.3、String

常见实例方法

  • 实例属性 length:用来获取字符串的长度(重点)
  • 实例方法 split('分隔符'):用来将字符串拆分成数组(重点)
  • 实例方法 substring(需要截取的第一个字符的索引号[, 结束的索引号]):用于字符串截取(重点)
  • 实例方法 startsWith(检测字符串[, 检测位置索引号]):检测是否以某字符开头(重点)
  • 实例方法 includes(搜索的字符串[, 检测位置索引号]):判断一个字符是否包含在另外一个字符串中,根据情况返回 truefalse(重点)
  • 实例方法 toUpperCase:用于将字母转换成大写
  • 实例方法 toLowerCase:用于将字母转换成小写
  • 实例方法 indexOf:检测是否包含某字符
  • 实例方法 endsWith:检测是否以某字符结尾
  • 实例方法 replace:用于替换字符串,支持正则表达式
  • 实例方法 match:用于查找字符串,支持正则表达式

4.2.3、Number

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

常用方法:toFixed() 设置保留小数位的长度,四舍五入

// 数值类型
const price = 12.345
// 保留两位小数  四舍五入
console.log(price.toFixed(2))  // 12.35

5、编程思想

5.1、面向过程编程(POP)

  • 面向过程就是分析出解决问题的步骤,然后用函数把这些步骤一步一步实现的,使用的时候再一个一个的依次调用就可以了。
  • 优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程
  • 缺点:没有面向对象易维护、易复用、易拓展

5.2、面向对象编程(OOP)

  • 面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。是以对象功能来划分问题,而不是步骤。
  • 面向对象程序开发思想中,每一个对象都是功能中心,具有明确的分工。
  • 面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
  • 面向对象的特性:封装性、继承性、多态性
  • 优点:易维护、易复用、易拓展,由于面向对象由封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
  • 缺点:性能比面向过程低

6、构造函数

  • 封装是面向对象思想中比较重要的一部分,JS 面向对象可以通过构造函数实现的封装。
  • 同样的变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的
function Star(uname, age) {
    this.uname = uname
    this.age = age
    this.sing = function () {
        console.log('我会唱歌')
    }
}
// 实例对象,获得了构造函数中封装的所有逻辑
const ldh = new Star('刘德华', 18)
const zxy = new Star('张学友', 19)
  • 总结:

    • 构造函数体现了面向对象的封装特性

    • 构造函数实例创建的对象彼此独立、互不影响

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

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

7、原型

7.1、原型对象

  • 构造函数通过原型分配的函数是所有对象所共享的
  • JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所有我们也称为原型对象
  • 这个对象可以挂载函数,对象实例化不会多次创建原型上的函数,节约内存。
  • 我们可以把那些不变的方法,直接定义在 prototype 对象上。这样所有的对象的实例就可以共享这些方法。
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('刘德华', 18)
const zxy = new Star('张学友', 19)
ldh.sing()
  • 构造函数和原型对象中的 this 都指向 实例化的对象。
let that
function Person(name) {
    that = this
    this.name = name
}
const obj = new Person('name')
console.log(that === obj)  // true
  • 例子:自定义 数组扩展方法 求数组最大值
const arr = [1, 2, 3, 4]
Array.prototype.max = function () {
    // 展开运算符
    // 原型里面的this指向实例化对象
    return Math.max(...this)
}
console.log(arr.max())

7.2、constructor 属性

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

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

  • 使用场景

    • 如果有多个对象的方法,我可以给原型对象采取对象形式赋值。
    • 但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。
    • 此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
    function Star() {}
    Star.prototype = {
        sing: function () {}
    }
    console.log(Star.prototype.constructor)  // 指向 Object
    // ---------------------------------------------------
    function Star() {}
    Star.prototype = {
        // 手动利用 constructor 指回 Star 构造函数
        constructor: Star,
        sing: function () {}
    }
    console.log(Star.prototype.constructor)  // 指向 Star
    

7.3、对象原型

思考

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

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

注意

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

请添加图片描述

7.4、原型继承

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

// 父类
function People() {}
// 子类
function Man() {}
// 原型继承
Man.prototype = new People()  // 注意new
Man.prototype.constructor = Man  // 指回原来的构造函数

7.5、原型链

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

请添加图片描述

原型链(就是一个查找规则)

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

8、深浅拷贝

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

// 一个 pink 对象
const pink = {
    name: 'pink老师',
    age: 18
}
const red = pink
console.log(red)  // {name: 'pink老师', age: 18}
red.name = 'red老师'
console.log(red)  // {name: 'red老师', age: 18}
// 但是 pink 对象里面的name值也发生了改变
console.log(pink)  // {name: 'red老师', age: 18}

8.1、浅拷贝

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

浅拷贝:拷贝的是地址(只拷贝最外层的属性值)

常见方法

  • 拷贝对象Object.assgin() / 展开运算符 {...obj} 拷贝对象
  • 拷贝数组Array.prototype.concat() 或者 [...arr]

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

8.2、深拷贝

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

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

常见方法

  • 通过递归实现深拷贝

    • 函数递归:如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
      • 简单理解:函数内部自己调用自己,这个函数就是递归函数
      • 递归函数的作用和循环效果类似
      • 由于递归很容易发生 “栈溢出” 错误(stack overflow),所有必须要加退出条件 return
    const o = {}
    function deepCopy(newObj, oldObj) {
        for (let k in oldObj) {
            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]
            }
        }
    }
    
  • lodash / cloneDeep

    const o = _.cloneDeep(obj)
    
  • 通过 JSON.stringify() 实现

    const o = JSON.parse(JSON.stringify(obj))
    

9、异常处理

9.1、throw 抛异常

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

function counter(x, y) {
    if (!x || !y) {
        // throw ‘参数不能为空’
        throw new Error('参数不能为空')
    }
    return x + y
}
counter()

9.2、try / catch 捕获错误信息

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

try {
    // 可能引起报错的代码,要写到 try 里面
} catch (error) {
    // 如果报错,会执行这里的代码,但是不中断程序的执行
    console.log(error.message)
} finally {
    // 不管有没有报错,这里的代码都会执行
}

总结

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

9.3、debugger

我们可以通过 try / catch 捕获错误信息(浏览器提供的错误信息)
在代码中加入 debugger,可以直接跳转到断点,方便调试代码

10、处理 this

10.1、this 指向

10.1.1、普通函数

普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】

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

<script>
	'use strict'
    function fn() {
        console.log(this)  // undefined
    }
    fn()
</script>

10.1.2、 普通函数

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

  • 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是不一样的
  • 箭头函数中的 this 引用的就是最近作用域中的 this
  • 向外层作用域中,一层一层查找 this,直到有 this 的定义
console.log(this)  // window
// 箭头函数
const sayHi = function() {
    console.log(this)  // 该箭头函数中的 this 为函数声明环境中的 this 一致
}

注意情况1:在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window。因此 DOM 事件回调函数如果里面需要 DOM 对象的 this,则不推荐使用箭头函数

注意情况2:同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数(构造函数和原型对象的 this 都指向实例对象

总结

  • 函数内不存在 this,沿用上一级的
  • 不适用:构造函数、原型函数、DOM 事件函数 等等
  • 适用:需要使用上层 this 的地方
  • 使用正确的化,它会在很多地方带来方便,后面我们会大量使用慢慢体会

10.2、改变 this

JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向

  • call() 了解

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

    • 语法

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

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

    • 语法

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

    • 使用 bind() 方法不会调用函数,同时指定被调用函数中 this 的值

    • 语法

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

11、防抖 & 节流

11.1、防抖(debounce)

  • 防抖:单位时间内,频繁触发事件,只执行最后一次
  • 举个栗子王者荣耀回城,只要被打断就需要重新来
  • 使用场景
    • 搜索框搜索输入,只需要用户最后一次输入完,再发送请求
    • 手机号、邮箱验证输入检测

例子:要求:鼠标在盒子上移动,鼠标停止500ms之后,里面的数字才会变化+1

const box = document.querySelector('.box')
let i = 1
function mouseMove() {
    box.innerHTML = i++
    // 如果存在开销较大操作,大量数据处理,大量DOM操作,可能会卡顿
}
box.addEventListener('mousemove', mouseMove)
  • 实现方式

    • lodash 提供的防抖函数来处理

      _.debounce(func, [wait=0], [option=])
      // func:函数  wait:时间ms
      
      const box = document.querySelector('.box')
      let i = 1
      function mouseMove() {
          box.innerHTML = i++
      }
      box.addEventListener('mousemove', _.debounce(mouseMove, 500))  // 500ms之后采取加1
      
    • 手写一个防抖函数来处理(利用 setTimeout 来实现)

      • 声明一个定时器变量
      • 当鼠标每次滑动都先判断是否有定时器了,如果有定时器先清除以前的定时器
      • 如果没有定时器则开启定时器,记得存到变量里面
      • 定时器里面调用要执行的函数
      function mouseMove() {
          box.innerHTML = i++
      }
      // 手写防抖函数
      function debounce(fn, t) {
          let timer
          // 返回一个匿名函数
          return function() {
              if (timer) clearTimeout(timer)
              timer = setTimeout(function(){
                  fn()  // 加小括号来调用
              }, t)
          }
      }
      box.addEventListener('mousemove', debounce(mouseMove, 500))  // 500ms之后采取加1
      

11.2、节流(throttle)

  • 节流:单位之间内,频繁触发事件,只执行一次

  • 举个栗子

    • 王者荣耀技能冷却,期间无法继续释放技能
    • 和平精英换子弹期间不能射击
  • 使用场景

    • 高频事件:鼠标移动 mousemove、页面尺寸缩放 resize、滚动条滚动 scroll 等等
  • 实现方式

    • lodash 提供的节流函数来处理

      _.throttle(func, [wait=0], [option])
      
      const box = document.querySelector('.box')
      let i = 1
      function mouseMove() {
          box.innerHTML = i++
      }
      box.addEventListener('mousemove', _.throttle(mouseMove, 500))  // 500ms内只采取一次加1
      
    • 手写一个节流函数来处理(利用 setTimeout 来实现)

      • 声明一个定时器变量
      • 当鼠标每次滑动都先判断是否有定时器了,如果有定时器则不开启新的定时器
      • 如果没有定时器则开启定时器,记得存到变量里面
      • 定时器里面调用要执行的函数
      • 定时器里面清空定时器
      function mouseMove() {
          box.innerHTML = i++
      }
      // 手写节流函数
      function throttle(fn, t) {
          let timer = null
          // 返回一个匿名函数
          return function() {
              if (!timer) {
                  timer = setTimeout(function() {
                      fn()
                      timer = null  // 清空定时器
                  }, t)
              }
          }
      }
      box.addEventListener('mousemove', throttle(mouseMove, 500))  // 500ms内只采取一次加1
      

11.3、总结

性能优化说明使用场景
防抖单位时间内,频繁触发事件,只执行最后一次搜索框搜索输入、手机号、邮箱验证输入检测
节流单位时间内,频繁触发事件,只执行一次高频事件:鼠标移动 mousemove、页面尺寸缩放 resize、滚动条滚动 scroll 等等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值