作用域

局部作用域

  • 函数作用域
  • 块作用域 如 for 内

全局作用域

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

对于var变量,其声明后就是全局变量

  • var变量有变量提升的说法:
    • 如果使用了 还未 声明的变量,编译器会把 后面声明的变量的“声明”(注意只有声明,不会将赋值语句提升)提升到作用域的最前面

就近原则

JS垃圾回收机制

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

内存的生命周期

JS环境中分配的内存, 一般有如下生命周期:

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

堆栈空间分配区别:

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

垃圾回收算法

下面介绍两种常见的浏览器垃圾回收算法: 引用计数法(IE浏览器) 和 标记清除法(其实就是可达性分析法)

引用计数

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

算法:

  1. 跟踪记录被引用的次数
  2. 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
  3. 如果减少一个引用就减1 —
  4. 如果引用次数是0 ,则释放内存

存在一个致命的问题:嵌套引用(循环引用)

如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。

函数进阶

l

标记清除法(可达性分析法)

核心:从根部扫描对象,能查找到的就是使用的,查找不到的就要回收

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

闭包

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

简单理解:闭包 = 内层函数 + 外层函数的变量 (简单来说就是有一个函数套了一层函数,内部的函数调用了外部函数的局部变量)

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

function outer(){
    let i=0
    function fn(){
        i++
        console.log(i)
    }
    return fn
}

const fun = outer()
fun()// 1 相当函数外部访问了函数内部 的局部变量

这样其实简单实现了一个 统计 函数调用次数的 函数,每次通过fun调用,不会使 i 置零,有点类似 java中new了一个实例变量(里面的成员变量是私有的,外部不能直接访问)

函数进阶

函数提升

函数生命前就可以调用

编译器会把所有函数的声明放到当前作用域的最前面

但是函数表达式不能提升(函数表达式相当于变量的赋值,因为之提升了声明所以不能使用)

函数的参数

函数默认参数: function fun(x=10)

函数动态参数

  • 参数数量不定
function sum(){
    let s = 0
    for(let i=0;i<arguments.length;i++){
        s+=arguments[i];
    }
    return s;
}

console.log(sum(1,2,3,4))

arguments 是一个伪数组,只存在于函数中

函数剩余参数(展开运算符)

剩余参数允许我们将一个不定数量的参数表示为一个数组

  1. … 是语法符号,置于最末函数形参之前,用于获取多余的实参
  2. 借助 … 获取的剩余实参,是个真数组
function config(baseURL,..other){
    console.log(baseURL)
    console.log(other)// 接受成一个真数组
}

config('baidu.com','get','json')

单独使用…作为展开运算符使用,可以将数组展开

求数组最大最小值

Math.max(...nums)

合并数组

const arr3 = [...arr1,...arr2]

箭头函数

基本语法

const fn = function (){
    console.log(123)
}

// 1、箭头函数
const fn = () => {
    console.log(123)
}


//
const fn = (x,y)=>{
    函数体
}

// 调用
fn(x,y)


// 2、只有一个形参时可以省略小括号,(有多个参数或者没有参数都不能省略)
const fn = x =>{
    函数体
}
fn(x)

// 3、只有一行代码时可以省略大括号, 
const fn = x => console.log(x)

// 4、如果这一行就是返回语句,可以省略return 
const fn = x => x+x

// 5、箭头函数可以返回一个对象
const fn = (username) => ({name: username })

举例

阻止表单的默认提交事件

const form = document.querySelector('form')
form.addEventListener('click',ev=>ev.preventDefault())

箭头函数的this

普通的this 是 谁调用,this 就指向谁

重要:!!!!!箭头函数不会创建自己的this,只会从自己的作用域链的上一层沿用this(但是普通函数会创建自己的this)

<script> 
const fn = () => {
    console.log(this)
}
</script>

这里的this ,指的其实是windows(script 域 的this指的就是 windows)

<script> 
const obj = {
    uname: "Mr.D"
    sayHi: ()=>{
        console.log(this) // this 指向谁?
    }
}

obj.sayHi() // window
</script>


<script> 
const obj = {
    uname: "Mr.D"
    sayHi: function(){
        console.log(this) // this 指向谁?
        let i =1
        const sayHello = () =>{
            console.log(this) // this 指向谁?
        }
    }
}
答:
obj.sayHi() // obj 里面的sayHello 也是 obj
</script>

解构和赋值

数组结构

数组的解构:把数组元素快速批量赋值给变量

const [max,min,avg] = [100,60,80]

// 交换变量
let a=1
let b=2; 必须加分号
[b,a]=[a,b]

PS 必须加分号的情况

  • 两个立即执行函数之间用分号间隔
  • 数组结构之前的语句

对象结构

const obj = {
    uname:"taotaozi"
    age:18
}

// 解构
const {uname,age}={ uname:"taotaozi", age:18}
// 要求 结构的变量名与属性名相同。。。为了解决这种情况,结构的变量名可以改名 旧变量名: 新变量名
const {uname:username,age}={ uname:"taotaozi", age:18}

解构对象数组

const pig = [{
    uname:"佩奇"
    age:17
}]

const [{uname,age}]=pig

常见解析案例

const response = {
    "code":200,
    "msg":"获取对象元素列表成功",
    "data":[
        {
            "id":1,
            "name":"ttz"
        },
        {
            "id":2,
            "name":"ddz"
        }
    ]
    
}

// 获取数据部分
// const {data}= response // 可省略
//渲染
render({data}){ //传参同时解构 data是数组
    
}
// 为了避免冲突也可以改名
render({data:myData}){
    for(dataitem:myData){
        
    }
    // or
    myData.forEach(function(item,index){
        console.log(item)
        console.log(index)
    })
    // or 因为 item必须 index 可选
    myData.forEach(item=>{
        console.log(item)
    })
}

内置对象/包装数据类型

包装类的隐式转换

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

基本数据类型:

 字符串、数值、布尔、undefined、null

引用类型:

 对象

如 字符串 按理说 因为他不是对象,所以应该是没有 长度(length)等属性,但是却有;如 num.toFixed(2)

其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型

其实其内部可以这样理解:

cosnt str = 'pink'
// 底层应该经过了这样的转化
const str = new String('pink') // 底层自动帮我们转换的
console.log(str.length) //调用的是引用数据类型的属性和方法

内置对象

Object

  • Object 是内置的构造函数,用于创建普通对象。
  • Object.keys 静态方法获取对象中所有属性(键)
  • Object.values 静态方法获取对象中所有属性值
  • Object. assign 静态方法常用于对象拷贝//尝试用添加属性
const o = { name : 'peiqi',age :6}
// 普通遍历方式
for(let k in o){
    k     o[k]
}

// Object.keys 静态方法获取对象中所有属性(键),返回一个数组
const keys = Object.keys(o) // ['name','age']
const values = Object.values(o) // ['peiqi',6]

// 拷贝对象 从 o 到 o1
const o1={}
Object.assign(o1,o)

Array

const arr = Array(3,5)

const arr = [3,5]

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

总结:

  1. 推荐使用字面量方式声明数组,而不是 Array 构造函数

  2. 实例方法 forEach 用于遍历数组,替代 for 循环 (重点)

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

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

  5. 实例方法 join 数组元素拼接为字符串,返回字符串(重点)

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

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

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

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

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

  11. 实例方法 splice 删除或替换原数组单元

  12. 实例方法 reverse 反转数组

  13. 实例方法 findIndex 查找元素的索引值

reduce

arr.reduce(function(){return?},起始值)

arr.reduce(fucntion(上一次值,当前值){ return ?},起始值)

起始值可以省略,那么起始值就是数组第一项,当前值是第二项

举例

const arr = [1,3,5]
const count = arr.reduce((prec,item)=>prev+item)
console.log(count)

其他 从 mdn 文档查找吧

包装数据类型

String

String 是内置的构造函数,用于创建字符串。

<script>
  // 使用构造函数创建字符串
  let str = new String('hello world!');

  // 字面量创建字符串
  let str2 = '你好,世界!';

  // 检测是否属于同一个构造函数
  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 用于设置保留小数位的长度

深浅拷贝

浅拷贝

如果直接 用 直接赋值 作为复制对象的方法 , 其实 复制的是堆里面对象的地址

常见方法:

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

直接赋值浅拷贝有什么区别?

  • 直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址

  • 浅拷贝如果是一层对象(对象里面全是基本属性,没有对象套对象),不相互影响,如果出现多层对象拷贝还会相互影响

浅拷贝怎么理解?

  • 拷贝对象之后,里面的属性值是简单数据类型直接拷贝值

  • 如果属性值是引用数据类型则拷贝的是地址

深拷贝

递归实现深拷贝

函数递归:

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

  • 简单理解:函数内部自己调用自己, 这个函数就是递归函数
  • 递归函数的作用和循环效果类似
  • 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
<body>
  <script>
    const obj = {
      uname: 'pink',
      age: 18,
      hobby: ['乒乓球', '足球'],
      family: {
        baby: '小pink'
      }
    }
    const o = {}
    // 拷贝函数
    function deepCopy(newObj, oldObj) {
      debugger
      for (let k in oldObj) {
        // 处理数组的问题  一定先写数组 在写 对象 不能颠倒
        if (oldObj[k] instanceof Array) {
          newObj[k] = []
          //  newObj[k] 接收 []  hobby
          //  oldObj[k]   ['乒乓球', '足球']
          deepCopy(newObj[k], oldObj[k])
        } else if (oldObj[k] instanceof Object) {
          newObj[k] = {}
          deepCopy(newObj[k], oldObj[k])
        }
        else {
          //  k  属性名 uname age    oldObj[k]  属性值  18
          // newObj[k]  === o.uname  给新对象添加属性
          newObj[k] = oldObj[k]
        }
      }
    }
    deepCopy(o, obj) // 函数调用  两个参数 o 新对象  obj 旧对象
    console.log(o)
    o.age = 20
    o.hobby[0] = '篮球'
    o.family.baby = '老pink'
    console.log(obj)
    console.log([1, 23] instanceof Object)
    // 复习
    // const obj = {
    //   uname: 'pink',
    //   age: 18,
    //   hobby: ['乒乓球', '足球']
    // }
    // function deepCopy({ }, oldObj) {
    //   // k 属性名  oldObj[k] 属性值
    //   for (let k in oldObj) {
    //     // 处理数组的问题   k 变量
    //     newObj[k] = oldObj[k]
    //     // o.uname = 'pink'
    //     // newObj.k  = 'pink'
    //   }
    // }
  </script>
</body>

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>

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>

异常处理

抛出

总结:

  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. try...catch 用于捕获错误信息
  2. 将预估可能发生错误的代码写在 try 代码段中
  3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息

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>

debugger

相当于断点调试

处理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
  user.sayHello()
</script>

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

箭头函数

箭头函数中的 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

同样由于箭头函数 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

使用 call 方法调用函数,同时指定函数中 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)
</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 的值
  let sayHello = sayHi.bind(user);
  // 调用使用 bind 创建的新函数
  sayHello()
</script>

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

防抖节流

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