JS高级讲解

JS高级

一.作用域

1.介绍

作用域规定了变量能够访问的范围,离开了这个范围变量不能被访问

作用域分为:
局部作用域
全局作用域

2.局部作用域

1.局部作用域–函数作用域

    function fn() {
      //这个a,就是局部变量,只能在函数内部使用
      let a = 10
    }
    fn()
    console.log(a) // 报错,a没有定义

总结:
1.函数内部声明的变量,在函数外部无法被访问
2.函数的参数也是函数内部的局部变量
3.不同函数内部声明的变量无法互相访问
4.函数执行完毕后,函数内部的变量实际被清空了

2.局部作用域 - 块作用域(块作用域是ES6的东西,只有let、const声明的变量也有块作用域)
块就是一对大括号,比如 {} 、 if() {} 、for(…) {}

{
      这里的大括号就形成了一个块级作用域
      这里声明的变量,有效范围就是这个大括号里面
      let b = 20
      console.log(b)
    }

    console.log(b) // 这里报错

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

注:开发中 letconst 经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const 声明成常量。

3.全局作用域

写在script标签内部,或者外部引入的js文件中的变量

局部作用域可以使用全局变量

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

4.作用域链

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

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

5.垃圾回收机制

简称GC

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

如果不了解JS的内存管理机制,容易造成内存泄露

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

内存的生命周期:
1.内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
2.内存使用:即读写内存,也就是使用变量、函数等
3.内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
4.说明:
全局变量一般不会回收(关闭页面回收);
一般情况下局部变量的值, 不用了, 会被自动回收掉

常见的两种算法:引用计数法和标记消除法

引用计数
IE采用的引用计数算法, 定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用
算法:
1.跟踪记录每个值被引用的次数。
2.如果这个值的被引用了一次,那么就记录次数1
3.多次引用会累加。
4.如果减少一个引用就减1。
5.如果引用次数是0 ,则释放内存。

存在问题

function fn() {
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return '引用计数无法回收'
}
fn()
因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露

标记清除法
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
核心:
1.标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
2.就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。
3.那些无法由根部出发触及到的对象被标记为不再使用,稍后进 行回收。

6.闭包

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

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

二.函数

1.变量提升

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

var会把创建、声明提升到前面,let只会把创建过程提升到前面,如果是创建之后,声明之前使用变量,就会进入到暂存性死区

2.函数提升

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

代码运行的时候,会把函数的声明、创建提升到当前作用域的最开头
然后会把变量的声明过程(没有赋值过程)提升到当前作用域的最开头

3.函数参数

(1)动态参数

当不确定传入参数个数时,可以arguments,但是推荐使用剩余参数

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 的作用是动态获取函数的实参

(2)剩余参数
<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.借助 ... 获取的剩余实参,是个真数组

(3)展开运算符

1.在构造数组时,能够将其他数组或字符串展开

let arr1=[3,4]
let arr2=[5,6]
let arr=[1,2,...arr1,...arr2,7,8,'abc']

2.在为函数传递参数时,能够将其他数组或字符串展开

let arr3=[2,8,7,5,3]
function fn(...a){
console.log(a)
}
fn(...arr3)

3.在构造字面量对象的时候,能够展开其他对象

let obj1 = { uname: 'zs', age: 20 }
    let obj2 = { sex: '男', height: 180, age: 30 }
    let obj = {
      id: 100,
      ...obj1,
      ...obj2,
      weight: 75
    }
(4)默认值
<script>
  // 设置参数默认值
  function sayHi(name="小明", age=18) {
    document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
  }
  // 调用函数
  sayHi();
  sayHi('小红');
  sayHi('小刚', 21);
</script>

总结:

1.声明函数时为形参赋值即为参数的默认值
2.如果参数未自定义默认值时,参数的默认值为 undefined
3.调用函数时没有传入对应实参时,参数的默认值被当做实参传入

(5)箭头函数

是ES6中的新语法,新的函数声明方式,简化代码

箭头函数属于表达式函数,因此不存在函数提升

语法:

()=>{}
let fn=()=>{}
let fn=(a,b)=>{
	console.log(123)
}

简化写法
箭头函数只有一个参数时可以省略圆括号()
箭头函数函数体只有一行代码时可以省略花括号{},并自动作为返回值被返回

let fn=x=>{
	return x*x
}
let fn = x => x*x

特别注意,{}语义不明(函数的大括号还是对象的大括号)的时候加一个()
let fn = () => ({ uname: ‘zs’, age: 20 })

其他特点:
箭头函数中没有arguments,只能使用…动态获取实参
箭头函数内部,没有自己的this,箭头函数中的this,把它当作普通变量,使用的时候按照作用域去查找
箭头函数,不能当做构造函数

三.解构赋值

1.数组解构

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

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

总结:

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

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

2.对象的解构

  1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  2. 对象属性的值将被赋值给与属性名相同的变量
  3. 注意解构的变量名不要和外面的变量名冲突否则报错
    4.对象中找不到与变量名一致的属性时变量值为 undefined
    let { sex, uname, ...other } = {
      uname: 'zs',
      age: 20,
      sex: '男',
      height: 180,
      weight: 80
    }
  console.log(sex, uname, other)
  
  
  let person = {
      uname: 'zs',
      age: 34,
      dog: {
        name: '汪仔',
        age: 3
      },
      cat: {
        name: '小花',
        age: 2
      }
    }
   
   let { uname, cat: { name } } = person
   console.log(uname, name)    

为函数传参

let obj={
	uname:'aa',
	age:20,
	sex='男'
}
//let {sex}={uname:'zs',age:20,sex:'男'}
function fn({sex}){
	console.log(a)
}
fn(obj)

为变量定义别名

    let age = 20

    // let { uname, age: 其他名字 } = { uname: 'zs', age: 100 }
    let { uname, age: nianling } = { uname: 'zs', age: 100 }

    console.log(nianling)

补充–数组方法

1.forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数
主要使用场景: 遍历数组的每个元素

 [4, 5, 8, 9].forEach(函数)

    [4, 5, 8, 9].forEach(() => {})

    [4, 5, 8, 9].forEach((item, index, self) => {
      item 表示数组中的每一项,第1次循环,item=4; 第2次循环item=5 第3次循环item=8 第4次循环item=9
      index 表示数组的每一项的下标(索引),这个形参可选
      self 表示当前的这个数组 [4, 5, 8, 9],这个形参可选
    })

举例:

 [4, 5, 8, 9].forEach((item, index, self) => {
      console.log(item)
    })

    [4, 5, 8, 9].forEach((item) => {
      console.log(item)
    })

    [4, 5, 8, 9].forEach(item => console.log(item))

2.filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组

filter() 筛选数组
返回值:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组
参数:currentValue 必须写, index 可选
因为返回新数组,所以不会影响原数组

    let arr = [4, 10, 5, 8, 2, 9]
    // 希望筛选数组的元素,选出大于7的元素
    let result = arr.filter((item, index, self) => {
      // return 条件
      return item > 7
    })

    let result = arr.filter(item => item > 7)

    console.log(result) // [10, 8, 9]

四.深入对象

1.创建对象的三种方式

1.利用对象字面量创建对象

const o={
	name:'佩奇'
}
    let obj = {
      uname: 'zs',
      'user-name': 'lisi',
      age: 20,
      bb: {
        dname: '汪仔'
      },
      cc: ['琪琪', 'pink'],
      dd: null,
      ee: true,
      ff: function () { },
      gg: function () { },
    }
    // obj.属性
    console.log(obj.uname)
    // obj['属性']
    console.log(obj['user-name'])
    // obj[变量]
    let a = 'age'
    console.log(obj[a]) // 相当于是 obj['age']    

2.利用new Object创建对象

const o = new Object({name:'佩奇'})
console.log(o)

3.通过构造函数创建对象

 // 构造函数,本质肯定也是函数(构造函数要求首字母大写)
    function Pig(name, age, gender) {
      // 把对象的属性,放到构造函数内部,写一份即可
      // 把对象的所有属性,都加给 this
      this.name = name
      this.age = age
      this.gender = gender
    }

    // 通过构造函数,创建对象语法 【创建对象,也叫做 实例化对象】
    // let 实例对象 = new 构造函数(参数, 参数, ....)
    let Peppa = new Pig('佩奇', 6, '女')
    let George = new Pig('乔治', 3, '男')
    console.log(Peppa)
    console.log(George)
  1. 使用 new 关键字调用函数的行为被称为实例化
  2. 实例化构造函数时没有参数时可以省略 ()
  3. 构造函数的返回值即为新创建的对象
  4. 构造函数内部的 return 返回的值如果是字符串什么的无效,但是返回的是对象,则实例化之后得到这个对象。
    注:实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。

2.静态属性 和静态方法

给构造函数添加的属性或方法,叫静态方法。
静态方法只能通过构造函数来调用

function Person(name,age){
	//省略实例对象
}
//静态属性
Person.eyes=2
Person.arms=2
//静态方法
Person.walk=function(){
	console.log('^_^')
	//this指向Person
	console.loig(this.eyes)
}
  1. 构造函数的属性和方法被称为静态成员
  2. 一般公共特征的属性或方法静态成员设置为静态成员
  3. 静态成员方法中的 this 指向构造函数本身

3.包装类型

基本数据类型:
字符串、数值、布尔、undefined、null
引用类型:
对象
但是,我们会发现有些特殊情况:

const str = 'andy'
console.log(str.length)

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

    let str = 'hello'
    console.log(typeof str) // string

    str = new String('hello') // JS内部,自动 new String(),得到一个对象
    console.log(typeof str) // object

4.数组方法

  1. 推荐使用字面量方式声明数组,而不是 Array 构造函数
    // Array.from -- 将伪数组,转成真数组(伪数组不能调用数组方法;转成真数组就可以调用数组方法了)
    let lis = document.querySelectorAll('li') // 得到伪数组
    console.log(lis)
    // // lis.pop() // 报错,不能通过伪数组,调用数组方法
    let arr = Array.from(lis)
    arr.pop() // 移除数组中最后一个元素
    console.log(arr)
    
     // Array.isArray() -- 判断变量是否是数组
    console.log(Array.isArray([])) // true
    console.log(Array.isArray(['a', 'b'])) // true
    let lis = document.querySelectorAll('li') // 得到伪数组
    console.log(Array.isArray(lis)) // false
    console.log(Array.isArray({ uname: 'zs' })) // false
  1. 实例方法 forEach 用于遍历数组,替代 for 循环 (重点)

  2. 实例方法 filter 过滤数组单元值,生成新数组(重点)

  3. 实例方法 map 迭代原数组,生成新数组(重点)

    let arr = [3, 4, 6, 9]
    let res = arr.map(item => {
      // return '结果'
      return item * item
    })
    console.log(res) // [9, 16, 36, 81]
  1. 实例方法 join 数组元素拼接为字符串,返回字符串(重点)

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

    let arr = [100, 150, 230, 99, 500, 600]
    let res = arr.find(item => {
      // return '条件'
      return item > 200
    })
    console.log(res) // 230,结果是第1个找到的元素
  1. 实例方法every 检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)
    let arr1 = [3, 4, 6, 9]
    let res1 = arr1.every(item => {
      // return '条件'
      return item > 5
    })

  1. 实例方法some 检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false
    let arr = [3, 4, 6, 9]
    let res = arr.some(item => {
      // return '条件'
      return item > 5
    })

    console.log(res) // true

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

  2. 实例方法 sort 对原数组单元值排序

    let arr = [3, 5]
    let res = arr.sort((a, b) => {
      // a = 5  b = 3
      // return 正数, 则不交换两个元素的位置
      // return 负数, 则交换两个元素的位置
      // return 0,什么都不做
      return -2
    })
    console.log(res) // [5, 3]

    // ---------------- 数字的排序 ---------------------
    // 目标:实现从小到大排序
    let arr = [3, 5, 4, 8, 0, 2]
    let res = arr.sort((a, b) => {
      // 第1次比较,a = 5; b = 3,这次不希望交换位置,则返回正数  a - b
      // 第2次比较,a = 4; b = 5,这次希望交换位置,则返回负数 a - b
      return a - b
    })
    console.log(res)

    // ---------------- 复杂数字的排序 -------------------
    let arr = [
      { uname: 'zs', age: 20 },
      { uname: 'lisi', age: 30 },
      { uname: 'wangwu', age: 10 },
      { uname: 'pink', age: 40 },
    ]
    // 目标:按照年龄,从小到大排序(升序)
    let res = arr.sort((a, b) => {
      // 第1次比较,a = { uname: 'lisi', age: 30 }   b = { uname: 'zs', age: 20 }
      // 第1次比较,不希望交换位置。需要返回正数,怎么得到正数 a.age - b.age
      return a.age - b.age
    })
    console.log(res)


    // -------------- 按照英文字符或中文排序 --------------
    // 中文、英文也有大小之分,按照 Unicode 编码进行大小比较的
    // 世界上所有的字符(拉丁文、英文、中文、日文等等,包括符合),都对应着一个独一无二的Unicode编码
    // 完整的Unicode编码:http://www.tamasoft.co.jp/en/general-info/unicode.html
    // 中文的Unicode编码范围是 4e00 ~ 9fa5
    // 按照姓名,从小到大排序
    let arr = [
      { uname: 'zs', age: 20 },
      { uname: 'lisi', age: 30 },
      { uname: 'wangwu', age: 10 },
      { uname: 'pink', age: 40 },
    ]
    let res = arr.sort((a, b) => {
      // 第1次,a = { uname: 'lisi', age: 30 }; b = { uname: 'zs', age: 20 }
      // 
      if (a.uname < b.uname) return -1  // lisi < zs ,这种情况下,我们需要交换位置,所以才返回 -1
      if (a.uname > b.uname) return 1
      if (a.uname === b.uname) return 0
    })
    console.log(res)

    // --------------------------- 打乱数组 ------------------------
    let arr = [3, 5, 2, 8, 9, 7]
    let res = arr.sort((a, b) => {
      return Math.random() - 0.5
    })
    console.log(res)
  1. 实例方法 splice 删除或替换原数组单元
    // splice -- 删除指定的元素,并且可以添加新元素
    // 语法: arr.splice(位置, 删除的数量, 新增的元素, 新增的元素, ........)
    // 这个方法,直接修改原数组
    let arr = ['宋江', '吴用', '林冲']
    // --- 删除一个元素 ---
    arr.splice(1, 1) // 从下标1的位置,删除1个元素
    console.log(arr)
    // --- 修改一个元素 ---
    arr.splice(1, 1, '花荣')
    console.log(arr) // ['宋江', '花荣', '林冲']
  1. 实例方法 reverse 反转数组

  2. 实例方法 findIndex 查找元素的索引值,找不到返回-1

    let arr = [100, 150, 230, 99, 500, 600]
    let res = arr.findIndex(item => {
      // return '条件'
      return item > 200
    })
    console.log(res) // 2,结果是第1个找到的元素的下标
  1. 实例方法’reduce’ 累计器 返回函数累计处理的结果,经常用于求和等
    有初始值
    let arr = [3, 7, 4, 6, 5]
    // let sum = arr.reduce((总和, 当前元素) => { }, 初始值)
    let sum = arr.reduce((total, curr) => {
      // 因为有初始值,所以起始的时候,total = 初始值100
      return total + curr  // 第1次循环 100 + 3 = 103  第2次循环 103 + 7 = 110 ...........
    }, 100)
    console.log(sum)

没有初始值

    let arr = [3, 7, 4, 6, 5]
    // let sum = arr.reduce((总和, 当前元素) => { }, 初始值)
    let sum = arr.reduce((total, curr) => {
      // 因为[没有]初始值,所以【起始】的时候,【total = 第1个元素,也就是3】 【curr = 第2个元素,也就是7】
      return total + curr  // 第1次循环 3 + 7 = 10  第2次循环 10 + 4 = 14 ...........
    })
    console.log(sum)
  1. unshift:向数组开头添加新元素
  2. push:向数组的末尾添加新元素
  3. shift:从数组开头删除一个元素
  4. pop:从数组的末尾删除一个元素

5.字符串方法

  1. 实例属性length用来获取字符串的度长(重点)
  2. 实例方法split(‘分隔符’)用来将字符串拆分成数组(重点)
    let str = '小a,小b,小c'

    // ['小a', '小b', '小c']
    let arr = str.split(',')
    console.log(arr)
  1. 实例方法substring(需要截取的第一个字符的索引[,结束的索引号])用于字符串截取(重点)
    let res = str.substring(2) // 表示从下标2的位置开始截取,一直截取的结尾
    console.log(res) // ,小b,小c
    let res2 = str.substring(2, 6) // 表示从下标2的位置开始截取,截取到下标6的位置(不包括6的位置)
    console.log(res2) // 
  1. 实例方法startswith(检测字符串[,检测位置索引号])检测是否以某字符开头(重点)
    console.log('hello'.startsWith('h')) // true
    console.log('hello'.startsWith('he')) // true
    console.log('hello'.startsWith('hello')) // true
    console.log('hello'.startsWith('e')) // false
    console.log('hello'.startsWith('e', 1)) // true。从位置1开始算起,看这段字符串的开头是否是e
  1. 实例方法includes(搜索的字符串[,检测位置索引号])判断一个字符串是否包含在另一个字符串中,根据情况返回true或false(重点)
    console.log('hello'.includes('h')) // true
    console.log('hello'.includes('ell')) // true
    console.log('hello'.includes('ho')) // false
    console.log('hello'.includes('h', 2)) // false 从2的位置往后数,看字符串中是否包含h
  1. 实例方法toupperCase用于将字母转换成大写
console.log('hello你好'.toUpperCase()) // HELLO你好
  1. 实例方法toLowerCase用于将就转换成小写
console.log('This Is A JS'.toLowerCase()) // this is a js
  1. 实例方法indexof检测是否包含某字符
  2. 实例方法endsWith检测是否以某字符结尾
    console.log('hello'.endsWith('llo')) // true
    console.log('hello'.endsWith('ll', 4)) // true,4个长度,意思是先截取4个字符,然后看这4个字符的结尾是否是ll
  1. 实例方法replace用于替换字符串,支持正则匹配
  2. 实例方法match用于查找字符串,支持正则匹配
  3. 实例方法trim用于去除字符串两边的空白
    console.log('    hello   ')
    console.log('    hello   '.trim())
  1. 实例方法search在字符串中搜索字符,如果有这个字符,则返回第1个找到的位置;没有找到返回-1
    console.log('hello'.search('e')) // 1
    console.log('hello'.search('l')) // 2
    console.log('hello'.search('a')) // -1
  1. 实例方法replace执行字符串替换,支持正则匹配
    console.log('this is a js'.replace('s', 'a')) // thia is a js // 只找第1个s,然后替换
    console.log('this is a js'.replace(/s/g, 'a')) // thia ia a ja // 替换全部s

6.Number

toFlexd:去掉多余的小数,四舍五入,保留几位小数

    let num = 3.1415
    console.log(num.toFixed(2)) // 3.14
    console.log(num.toFixed(3)) // 3.142

精度问题:JS中使用小数计算经常出现精度不准确的问题,不是程序员的问题,是JS设计问题
解决方法:
看看有几位小数,然后乘10、100什么的转成整数
结果再除以相应倍数

7.Object

静态方法就是只有构造函数Object可以调用的

1.Object.values 静态方法获取对象中所有属性值

const o={name:'佩奇',age:6}
const arr=Object.values(o)
console.log(arr)//['佩奇',6]

2.Object. assign 静态方法常用于对象拷贝

//拷贝对象把o拷贝给obj
const o={name:'佩奇',age:6}
const obj={}
Object.assign(obj,o)
console.log(obj)//{name:'佩奇',age:6}

Object. assign 给对象添加属性

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

五.面向对象和面向过程

1.面向过程介绍

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

2.面向对象介绍

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

2.面向对象的特征及优缺点

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
面向对象的特性:
封装性
继承性
多态性

3.对比

面向过程编程:
优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。
缺点:没有面向对象易维护、易复用、易扩展

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

4.构造函数

封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。
同样的将变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的

function Star(uname,age){
	this.uname=unme
	this.age=age
	this.sing=function(){
		console.log('我会唱歌')
	}
}
const ldh=new Star('刘德华',18)
const zxy=new Star('张学友',19)

存在的问题:存在浪费内存的问题

console.log(ldh.sing==zxy.sing)//false

5.原型对象

任何一个函数(普通函数、构造函数、内置的Array、String),都有原型对象
原型对象由系统分配,不用程序员手动加

console.dir(Star)//输出,但是能看到内部 结构,其中Star.prototype就是原型对象

添加方法

    Star.prototype.say = function () {
      console.log('会说话')
    }

    Star.prototype.eat = function () {
      console.log('会吃饭')
    }

6.constructor 属性

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

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

使用场景:
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值.
但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了
此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

function Star(name){
	this.name=name
}
Star.prototype={
	sing:function(){console.log('唱歌')};
	dance:function(){console.log('跳舞')};
}
console.log(Star.prototype.constructor)//指向object

Star.prototype={
	//手动利用constructor指回Star构造函数
	constructor:Star,
	sing:function(){console.log('唱歌')};
	dance:function(){console.log('跳舞')};
}
console.log(Star.prototype.constructor)//指向Star

7.对象原型

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

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

8.原型继承

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

  1. 封装-抽取公共部分
    把男人和女人公共的部分抽取出来放到人类里面
const People={
	head:1,
	eyes:2,
	legs:2,
	say:function(){},
	eat:function(){}
}
function Man(){

}
function Woman(){
	this.baby=function(){}
}
  1. 继承-让男人和女人都能继承人类的一些属性和方法
    把男人女人公共的属性和方法抽取出来 People
    然后赋值给Man的原型对象,可以共享这些属性和方法
    注意让constructor指回Man这个构造函数
//把公共的属性和方法给原型
Man.prototype=People
//注意让原型里面的constructor重新指回Man
Man.prototype.constructor=Man

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

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

5.解决:
需求:男人和女人不要使用同一个对象,但是不同对象里面包含相同的属性和方法
答案:构造函数
new 每次都会创建一个新的对象

function Star(){
	age=18
}
const ldh=new Star()
const zxy=new Star()

9.原型链

原型链研究对象属性的查找机制

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

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

10.instanceof运算符

语法:

实例对象 instanceof 构造函数
p instanceof Pig   true/false

instanceof运算符用于检测实例对象的原型链上,是否有语法中给出的构造函数

六.对象的拷贝

1.直接赋值

原始类型的值,直接赋值之后,只是把存储的值赋值给变量,赋值之后,两个变量互不影响

引用类型的值,因为存储的是地址,等号赋值之后,是把地址赋值给另一个变量

2.浅拷贝

拷贝:把两个对象完全分开,修改一个对象,另外一个不受影响

首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址,只关注对象的第一层,只拷贝对象第一层的属性和属性值

常见方法:
1.拷贝对象:Object.assgin() / 展开运算符 {…obj} 拷贝对象
2.拷贝数组:Array.prototype.concat() 或者 […arr]

  1. 直接赋值和浅拷贝有什么区别?
    直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址
    浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会相互影响
  2. 浅拷贝怎么理解?
    拷贝对象之后,里面的属性值是简单数据类型直接拷贝值
    如果属性值是引用数据类型则拷贝的是地址

3.深拷贝

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

方法:

  1. 通过递归实现深拷贝

  2. lodash/cloneDeep

  3. 通过JSON.stringify()实现

  4. 通过递归实现深拷贝
    函数递归:
    如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
    简单理解:函数内部自己调用自己, 这个函数就是递归函数
    递归函数的作用和循环效果类似
    由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return

  5. js库lodash里面cloneDeep内部实现了深拷贝

const obj={
	uname='pink',
	age:18,
	hobby:['篮球','足球'],
	family:{
		baby:'小pink'
	}
}
const o=_.cloneDeep(obj)
console.log(o)
o.family.baby='老pink'
console.log(obj)
  1. 通过JSON.stringify()实现
const obj={
	uname='pink',
	age:18,
	hobby:['篮球','足球'],
	family:{
		baby:'小pink'
	}
}
const o=JSON.parse(JSON.stringify(obj))
console.log(o)
o.family.baby='老pink'
console.log(obj)

七.异常处理

1.throw 抛异常

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

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 throw 使用,能够设置更详细的错误信息
    function sum(x, y) {
      if (x === undefined || y === undefined) {
        // 如果符合这个条件,说明调用函数的时候,没有传递参数
        // return console.log('参数不对') // 左右结构,右侧的代码先运算
        // throw '参数不对'
        // 实际使用,经常用throw 和 new Error() 配合
        // throw new Error('错误描述信息')
        throw new Error('参数不对')
      }
      return x + y
    }


    // console.log(sum(3, 5))
    console.log(sum())

2.try / catch语句

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

  1. try…catch 用于捕获错误信息
  2. 将预估可能发生错误的代码写在 try 代码段中
  3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
  4. finally 不管是否有错误,都会执行
    语法:
try{
把代码写在这里,如果没有异常,会正常执行,否则会抛出
}catch(e){
e就是try抛出的错误对象
e.message是固定属性,表示错误描述信息
}

举例:

    // _.abc()
    try {
      console.log(111) // 代码从上到下执行,这一行正常执行
      _.abc()   // 这里,调用了一个不存在的方法,发生异常,抛出异常;try里面后续的代码就不再执行了
      console.log(222) // 这行不执行
    } catch (e) {
      console.log(e.message) // 只输出错误描述信息
    }

    console.log(333) // 这行不受影响,正常执行
  </script>

3. debugger调试

    function fn() {
      console.log(11)
      debugger  // 这里加一个断点,用于调试
      console.log(22)
      console.log(33)
    }
    fn()

八.this指向及修改this指向

1.this指向总结

    // 1. 全局中的this
    console.log(this) // window

    // 2. 普通函数中的this
    function fn() {
      console.log(this) // window
    }
    fn()  // window.fn(),window调用了fn函数,所以fn中的this表示window

    // 3. 定时器中的this
    // setTimeout(function () {
    //   console.log(this) // window
    // }, 100)

    // 4. 事件处理函数中的this
    document.body.addEventListener('click', function () {
      console.log(this) // 事件源,此例中,表示body这个DOM对象
    })

    // 5. 构造函数中,以及对象方法中,包括原型对象的方法中,this都表示实例对象
    function Dog() {
      this.age = 20
      this.say = function () {
        console.log(this)
      }
    }

    Dog.prototype.eat = function () {
      console.log(this)
    }

    let d = new Dog()
    d.say()
    d.eat()

    // 6. 构造函数的静态方法中,this表示构造函数
    function Pig() {

    }
    // 给构造函数,直接添加的方法,叫做静态方法
    Pig.eat = function () {
      console.log(this)
    }
    // 调用的时候,只能使用构造函数调用
    Pig.eat()

1.全局中的this表示window
2.普通函数的this表示window
3.定时器中的this表示window
4.事件处理函数中的this表示事件簿
5.构造函数、对象方法、原型对象方法中的this,表示实例对象
6.静态方法中的this,表示构造函数

练习:

    // 用var声明的变量,自动加到window对象中,也就是说,可以 window.age 这样用
    // 换句话说, age就是widnow对象的一个属性。
    // alert('aaaa')
    var age = 10

    // 用let声明的变量,不会当做window对象的属性,也就是不能通过 window.age2 来使用
    // let age2 = 20

    // console.log(window.age) // 10
    // console.log(window.age2) // undefined

    let obj = {
      age: 20,
      say: function () {
        setTimeout(function () {
          console.log(this.age) // 定时器中的this表示widnow。 这里的this.age 就是 window.age的意思
        }, 100)
      }
    }

    obj.say()  
    var age = 10
    let obj = {
      age: 20,
      say: () => {
        // 函数作用域,外层就是全局作用域 (不要把对象的大括号当做一个作用域)
        console.log(this.age) // 10
      },
      eat: function () {
        // .... 这里的 this 表示 obj
        let fn = () => {
          console.log(this.age) // 箭头函数中没有this,所以这里的this指向eat方法中的this,即obj
        }
        fn()
      },
      wash: () => {
        function abc() {
          console.log(this.age)
        }
        abc()
      }
    }

    // obj.say()
    // obj.eat()
    obj.wash()

2.修改函数中的this指向

  1. call()
    语法:
fun.call(thisArg, arg1, arg2, ...)

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

 let obj = { age: 20 }

    function fn(x, y) {
      console.log(this)
      console.log(x + y)
    }
    // fn() // 正常调用函数,函数中的this表示 window
    // 语法:fn.call(新的this, 3, 4)
    fn.call(obj, 3, 4)
    // 总结:
    // 1. 函数.call() 表示调用函数,原函数fn得以调用了
    // 2. 修改了原函数中的this,改成call方法的第1个参数了
    // 3. 如果原函数有形参,可以通过call方法为原函数传递实参
  1. apply()

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

    let obj = { age: 20 }

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

    // // 语法:fn.apply(新的this, [3, 4])
    fn.apply(obj, [4, 8])
    // 总结:
    // 1. 函数.apply() 表示调用函数,原函数fn得以调用了
    // 2. 修改了原函数中的this,改成 apply 方法的第1个参数了
    // 3. 如果原函数有形参,可以通过 apply 方法为原函数传递实参,但是必须使用数组格式
  1. bind()
    bind() 方法不会调用函数。但是能改变函数内部this 指向
    语法:
    fun.bind(thisArg, arg1, arg2, …)
    thisArg:在 fun 函数运行时指定的 this 值
    arg1,arg2:传递的其他参数返回由指定的 this 值和初始化参数改造的 原函数拷贝 (新函数)
    因此当我们只是想改变 this 指向,并且不想调用这个函数的时候,可以使用 bind,比如改变定时器内部的this指向.
    let obj = { age: 20 }

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

    // 语法:fn.bind(新的this, 5, 9)
    // let a = fn.bind(obj, 5, 9)
    // a()
    fn.bind(obj, 5, 9)()
// 总结:
// 1. 函数.bind() 表示创建了一个新的函数,并且不会调用任何函数
// 2. 修改了新函数中的this,改成 bind 方法的第1个参数了
// 3. 如果原函数有形参,可以通过 bind 方法为新函数传递实参
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值