JS高级

slince 于 2023-08-10 发布

闭包 closure

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

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

function outer(){
	const a = 1
	function f(){
	// 内层函数用到了外面的变量a
	console.log(a)
	}
	f()
}
outer()
// 常见的闭包格式,外部可以访问使用 函数内部的变量
function outer(){
	let a = 100
	function fn(){
		console.log(a)
	}
	return fn
}

闭包应用:实现数据的私有

例如,做一个函数统计,调用次数,函数每调用一次,++

let count = 1
function fun(){
	count ++
	console.log(`函数被调用了${count}次`)
}
// 存在一个问题,就是 i 全局变量,很容易被修改
function fn(){
	let count = 1
	function fun(){
		count ++
		console.log(`函数被调用了${count}次`)
	}
	return fun
}

const res = fn()

// 实现了数据的私有,这样就可以有效解决问题,注意 i 并没有被js的垃圾回收机制回收,所以就存在一个内存泄漏问题

变量和函数提升

变量提升是JavaScript中,允许变量声明之前被访问(仅存在于var变量)

注意:

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

// 1. 把所有var声明的变量提升到当前作用域的最前面
// 2. 只提升声明,不提升赋值

函数提升

函数在声明之前即可被调用,函数提升只出现在相同作用域上

fun()
// 声明函数
function fun(){
console.log('声明之前即被调用')
}

fun() // 错误
var fun = function(){
	console.log('函数表达式不存在提升现象')
}

函数参数

动态参数

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

function (){
  let sum = 0;
  for(let i = 0; i < arguments.length; i++){
    sum += arguments[i]
  }
	console.log(sum)
}

sum(1,2,3) // 6
sum(1,2,3,4,5) // 15

// arguments 是一个*伪数组*,只存在于函数中
// arguments 的作用是动态获取函数的实参

剩余参数

  1. …是语法符号,用于获取多余的实参
  2. 是个真数组
function fun(a, b, ...other){
  console.log(a, b)  // 1 2
  console.log(other) // [3, 4, 5, 6]是个数组
}
fun(1, 2, 3, 4, 5, 6)

展开运算符

注意与剩余参数区分,语法符号相同只是用法不同

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

// 1. 求数组最大值
console.log(Math.max(...arr1)) // 4

// 2. 合并数组
const arr2 = [7, 8, 9]
const arr = [...arr1, ...arr2]
console.log(arr) // [1, 2, 3, 4, 7, 8, 9]

// 展开运算符主要是 数组展开
// 剩余参数 在函数内部使用

箭头函数this

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

console.log(this) // Window

// 普通函数的this
function fun(){
  console.log(this) // Window
}
fun()

// 对象里面函数的this
const obj = {
  name: 'xiaoming',
  age: 20,
  fun: function(){
    console.log(this) // {name: 'xiaoming', age: 20, fun: ƒ}
  }
}
obj.fun()


// 箭头函数的this
const fun = () => {
  console.log(this) // Window
}
fun() 


// 对象函数中箭头函数的this
const obj = {
  name: 'xiaoming',
  age: 20,
  fun: () =>{
    console.log(this) //Window
  }
}
obj.fun()


对象解构

const { uname, age } = { uname: 'xiaoming', age: 18 } 
// 等价于 const = uname = obj.uname
// 要求属性名和变量名一致
console.log(uname) // xiaoming
console.log(age) // 18

// 给新的变量名赋值
const { name: uname, age } = { name: 'xiaoming', age: 18 } 
console.log(uname)

// 多级对象解构
const per = {
  name: '小明',
  family: {
   mother: '小红'
  }
  age: 20
},
const {name, family:{mother}} = per
console.log(name +'---'+ mother)

forEach遍历数组

forEach就是遍历,加强版的for循环,它没有返回值

const arr = ['red', 'green', 'pink']
const result = arr.forEach(function(item, index){
  console.log(item) // red green pink
  console.log(index) // 0 1 2
})
console.log(result) // undefined

数组reduce累计方法

// 无初始值
const arr = [1, 3, 5]
const total = arr.reduce(function(pre,cur){
  return pre + cur
})
console.log(total) // 9

// 有初始值
const arr = [1, 3, 5]
const total = arr.reduce(function(pre,cur){
  return pre + cur
},10)
console.log(total) //19

// 箭头函数写法
const arr = [1, 3, 5]
const total = arr.reduce((pre,cur) => pre + cur,10)
console.log(total) //19

原型

主要是利用原型对象实现方法共享

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sing = function(){
  console.log('唱歌')
}

const p1 = new Person('小明', 22)
const p2 = new Person('小红', 23)
console.log(p1)
console.log(p2)

  //  Person {name: '小明', age: 22}
  //  age: 22
  //  name: "小明"
  //  [[Prototype]]: Object

  //  ** sing: ƒ ()  **
  //  constructor: ƒ Person(name, age)
  //  [[Prototype]]: Object
  //  Person {name: '小红', age: 22}

  //  age: 22
  //  name: "小红"
  //  [[Prototype]]: Object

  //  ** sing: ƒ ()  **
  //  constructor: ƒ Person(name, age)
  //  [[Prototype]]: Object
小结
1. 原型是一个对象称prototype为原型对象
2. 原型可以用来方法可以把那些不变的方法直接定义在prototype对象上
3. 构造函数和原型对象里面的this指向实例化的对象

let that
function Person(name){
  this.name = name 
  that = this
}
const p = new Person()
console.log(that === p) // true

constructor属性

function Person(name){
  this.name = name
}
const p = new Person('小明')
console.log(Person.prototype.constructor === Person) // true
console.log(p instanceof Person) // true
console.log(p.constructor === Person) // true
// p.constructor 指向 Person 本身

// 原型对象中的constructr属性指向构造函数本身

使用场景:

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

function Person(name){
  this.name = name
}
Person.prototype = {
 sing: function(){console.log('唱歌')},
 dance: function(){console.log('跳舞')}
}
console.log(Person.prototype.constuctor) // 指向Object


function Person(name){
  this.name = name
}
Person.prototype = {
 constructor: Person, // 手动利用constructor指向Person函数本身
 sing: function(){console.log('唱歌')},
 dance: function(){console.log('跳舞')}
}
console.log(Person.prototype.constuctor) // 指向 Person

原型链–查找规则

  1. 当访问一个对象的属性(方法)时,首先查找这个对象自身有没有该属性
  2. 如果没有就查找它的原型(也就是__proto__指向的prototype原型对象)
  3. 如果话没有就查找原型对象的原型(Object的原型对象)
  4. 以此类推一直找到Object为止

深浅拷贝

const person ={
  name:'小明',
  age:18
}
const p = person
console.log(p) // {name: '小明', age: 18}
p.age = 20
// 问题出现了,我只想改变p中的age,但是person中的age也跟着改变了
console.log(person) // {name: '小明', age: 20}

浅拷贝和深拷贝只针对引用类型 浅拷贝:拷贝的是地址

const person = {
  name: '小明',
  age: 18
}

// 浅拷贝-1
const p = {...person}
console.log(p) // {name: '小明', age: 18}
p.age = 20
console.log(person) // {name: '小明', age: 20}
console.log(person) // {name: '小明', age: 18}

// 浅拷贝-2
const p2 = {}
Object.assign(p2, person)
console.log(p2) // {name: '小明', age: 18}
p2.age = 20
console.log(p2) // {name: '小明', age: 20}
console.log(person) // {name: '小明', age: 18}


const obj = {
  name : 'tom',
  age: 19,
  family: {
    wife: 'jerry'
  }
}

const o = {}
Object.assign(o, obj)
o.age = 20
o.family.wife = 'jerk'
console.log(o) // age: 20 family: {wife: 'jerk'} name: "tom"
// 问题出现了,obj中的family也跟着改变了
console.log(obj) // age: 20 family: {wife: 'jerk'} name: "tom"

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

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

常见实现深拷贝:

  1. 通过递归实现深拷贝
  2. lodash/cloneDeep
  3. 通过JSON.stringify()实现

函数递归

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

但是,递归很容易发生“栈溢出”错误,所以必须要加退出条件return

function getTime(){
  document.querySelector('div').innerHTML = new Date().toLocaleString()
  setTimeout(getTime,1000)
}
getTime()

call apply bind

性能优化

防抖(debounce)

单位时间内,频繁触发事件,只执行最后一次

使用场景:

当你在 JavaScript 中需要实现防抖(debounce)功能时,你可以使用以下代码作为一个示例:

function debounce(func, delay) {
  let timerId;

  return function(...args) {
    clearTimeout(timerId);

    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// 示例函数
function search() {
  // 模拟搜索操作
  console.log('Searching...');
}

// 创建防抖函数
const debounceSearch = debounce(search, 500);

// 在输入框中绑定事件监听器
const input = document.querySelector('input');
input.addEventListener('input', debounceSearch);

在上面的示例中,debounce 函数接收两个参数:func 是要执行的函数,delay 是延迟的时间间隔。它返回一个新的函数,该函数会在指定的延迟时间内被调用。

在示例中,我们定义了一个 search 函数,它模拟了搜索操作。然后,我们使用 debounce 函数创建了一个名为 debounceSearch 的防抖函数,将 search 函数作为参数传递给它,并设置了延迟时间为 500 毫秒。

最后,我们使用 addEventListenerdebounceSearch 函数绑定到输入框的 input 事件上。这样,当用户在输入框中输入时,防抖函数将确保在用户停止输入一段时间后才执行搜索操作,从而减少了频繁触发搜索的次数。

``

节流(throttle)

单位时间内,频繁触发事件,只执行一次

以下是一个基本的JavaScript节流函数的示例:

function throttle(func, delay) {
  let timerId;
  let lastExecutedTime = 0;

  return function(...args) {
    const currentTime = Date.now();

    if (currentTime - lastExecutedTime < delay) {
      clearTimeout(timerId);
      timerId = setTimeout(() => {
        lastExecutedTime = currentTime;
        func.apply(this, args);
      }, delay);
    } else {
      lastExecutedTime = currentTime;
      func.apply(this, args);
    }
  };
}

这个throttle函数接受两个参数:func是要节流的函数,delay是延迟的时间间隔(以毫秒为单位)。它返回一个新的函数,该函数在指定的时间间隔内最多执行一次。

使用示例:

function handleScroll() {
  console.log('Scroll event');
}

const throttledScroll = throttle(handleScroll, 200);

window.addEventListener('scroll', throttledScroll);

在上面的示例中,我们定义了一个handleScroll函数来处理滚动事件。然后,我们使用throttle函数创建了一个节流的版本throttledScroll,并将其绑定到scroll事件上。

这样,当用户滚动页面时,handleScroll函数最多每200毫秒执行一次,以减少事件的触发频率。