闭包 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变量)
注意:
- 变量在未声明即被访问时会报语法错误
- 变量在var声明之前被访问,不会报错,变量值为undefined
- let/const 声明的变量不存在变量提升
- 变量提升出现在相同作用域中
- 实际开发中推荐先声明再访问变量
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 的作用是动态获取函数的实参
剩余参数
- …是语法符号,用于获取多余的实参
- 是个真数组
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
原型
主要是利用原型对象实现方法共享
- 构造函数通过原型分配的函数是所有对象所 共享的
- JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象,所以也称为原型对象
- 这个对象可以挂在函数,对象实例化不会多次创建原型上的函数,节约内存
- 可以把一些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法
- 构造函数和原型对象中this都指向实例化的对象
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
原型链–查找规则
- 当访问一个对象的属性(方法)时,首先查找这个对象自身有没有该属性
- 如果没有就查找它的原型(也就是__proto__指向的prototype原型对象)
- 如果话没有就查找原型对象的原型(Object的原型对象)
- 以此类推一直找到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. 浅拷贝怎么理解
* 拷贝对象之后,里面的属性值是简单数据类型直接拷贝的值
* 如果属性值是引用数据类型,则拷贝的是地址
深拷贝:拷贝的是对象,不是地址
常见实现深拷贝:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.stringify()实现
函数递归:
如果一个函数在内部可以调用自身,这个函数就是递归函数
但是,递归很容易发生“栈溢出”错误,所以必须要加退出条件return
function getTime(){
document.querySelector('div').innerHTML = new Date().toLocaleString()
setTimeout(getTime,1000)
}
getTime()
call apply bind
-
相同点: 都可以改变函数内部的this指向
- 区别点:
- call和apply会调用函数,并且改变函数内部this指向
- call和apply传递的参数不一样,apply必须传递数组形式[arg]
- bind不会调用函数,可以改变函数内部的this指向
- 主要应用场景:
- call调用函数并且可以传递参数
- apply经常跟数组有关系,比如借助于数学对象实现数组最大最小值
- bind不调用函数,但是还想改变this指向,比如改变定时器内部的this指向
性能优化
防抖(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 毫秒。
最后,我们使用 addEventListener
将 debounceSearch
函数绑定到输入框的 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毫秒执行一次,以减少事件的触发频率。