函数进阶(call,apply,bind,闭包等)

142 阅读7分钟

call和apply方法

作用:调用函数并改变函数内this的指向

语法:

两种方法都是属于函数对象的,所以必须是函数对象才能调用

functionName.call(obj,实参1,实参2...)
functionName.apply(obj,[参数1,参数2...])

区别:

  • 都是改变函数内this的指向
  • call方法参数是一个一个传递,而apply是传递一个参数数组

特殊情况:如果传递的第一个伪造对象是null或undefined,则函数内this会指向window全局对象

function test(a,b){
    console.log(this,a,b)
}  
test.call(null,10,20)
test.apply(undefined,[10,20])

什么时候用call或apply

理解:call或apply就是一种高端调用函数的方式

  1. 可改变函数内this指向
  2. 改变传参的方式

bind方法

  • bind的作用和call/apply差不多,都是改变函数内this的指向。区别在于,bind不是立即执行,而是返回一个新函数。
  • bind方法也是是属于函数对象的,所以必须是函数对象才可以调用。

functionName.bind(obj,实参数1,实参数2...)

function test(a, b) {
   console.log(this.age, 'a:', a, ' b:', b);
}

var obj = { age: 18 }
var fn = test.bind(obj, 50, 60) // 这里test并不会立即执行而是会返回一个函数
fn()

call/apply/bind三者区别:

  • 相同点:都可以改变函数内this的指向
  • 不同点:call/apply是立即执行的,而bind是返回一个新函数

闭包(重点)

什么是闭包

闭包(closure):闭包是指有权访问另一个函数作用域中的变量的函数

闭包是 一个函数以及其捆绑的周边环境状态(词法环境)的引用的组合。换而言之,闭包让开发者可以通过内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

/*
  由于只有内部函数B才可以访问到函数A中的局部变量,那么我们只需要把函数B作为函数A的返回值,就可以在函数A外部访问它的内部变量! 其中函数B就是闭包。
*/
function A(){
  var a = 10;
  function B(){
    console.log(a);
  }
  return B;
}
var result = A();W
result();  // 10

产生闭包的条件

只要子函数访问(引用)到了父函数作用域中的标识符(变量名或函数名)就会产生闭包(closure)。(可利用debug断点调试查看闭包的产生)

创建闭包的方式

创建闭包最常见的方式:

  • 创建一个父函数,父函数内再创建子函数,子函数读取父函数内的局部变量,最后返回子函数
function A() {
    var a = 100
    return function () {
        console.log(a)
    }
}
var fn = A()
fn()

闭包的特点

闭包可以对父函数内部的变量持续引用。即让一个变量始终保存在内存中,即闭包可以延长变量的生命周期

function A() {
    var a = 100
    return function () {
        console.log(a++)
    }
}

var fn = A()
fn() // 100
fn() // 101
fn() // 102

闭包的应用场景

闭包的特点决定了应用场景

保存变量!保存变量!保存变量!

  • 保存变量。结合IIFE机制
  • 一次性函数
// 将函数当作参数接收
function once(callback) {
    // 设置isCall变量表示该函数有没有被调用过
	var isCall = false
	return function () {
		if (!isCall) {
			isCall = true
			callback.apply(this,arguments)
			}
		}
}
  • 事件的防抖节流
  • 缓存函数
  • ......

闭包的缺点

由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,使用不当可能会导致内存泄漏问题

内存泄漏

即内存无法释放,由于疏忽或错误造成程序未能释放已经不在使用的内存引起的

内存溢出

内存被撑爆,爆栈

function A(){
  var a = 1;
  function B(){
    console.log(a++)
  }
  return B;
}

var f = A();
f(); // 1
f(); // 2
f(); // 3
f = null; //解除引用, 不再使用,手动释放内存

所以不管是函数、对象、数组,只要不用了就建议解除引用,让垃圾回收机制自动回收其占用的内存

js垃圾回收机制是自动的。 里面有一套垃圾回收的策略或算法:引用计数。

导致内存泄漏:1. 意外的全局变量 2. 没有关闭的时间器

高阶函数

满足高阶函数的两个条件之一:

  • 将函数作为参数,如时间器函数(setInterval,setTimeout)
  • 将函数作为函数的返回值。如bind函数

判断变量是否时数组

// 方法1
Array.isArray(变量)

// 方法2
变量.constructor === Array

// 方式3:获取变量的精确类型
Object.prototype.toString.call(变量)

深浅拷贝(克隆)

不同数据类型赋值区别:

  • 基本类型:按值传递,修改新的不会影响旧的

  • 引用类型:按址传递,修改新的会影响旧的

    var obj1 = { name: '张三', age: 18 }
    var obj2 = obj1
    obj2.name = '李四'
    console.log(obj1.name) // { name:'李四',age:18}
    

浅拷贝:浅拷贝只复制指向某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存。修改新对象会影响原对象。

深拷贝:深拷贝会在内存中创造一个一模一样的对象,新对象跟原对象不共享内存。修改新对象不会影响到原对象。

深拷贝的实现

实现深拷贝,必须开辟一个新的内存空间去存储新对象,这样修改复制出来的对象不会影响原对象。

function cloneDeep(target) {
    // 对递归的target判断是否是基本数据类型
    if (typeof target !== 'object') {
        return target
    }
    // 根据传入的target初始化一个data=[]或data={}
    var data = Array.isArray(target) ? [] : {}
    // 根据target类型使用数组复制或对象复制
    if (Array.isArray(target)) {
        for (var i = 0; i < target.length; i++) {
            // 递归
            data[i] = cloneDeep(target[i])
        }
    } else {
        for (var k in target) { // k为对象中的属性名
            data[k] = cloneDeep(target[k])
        }
    }
    return data
}
var obj1 = {
    name: '张三',
    age: 20,
    hobby: ['吃饭', '睡觉', { say: 'hello' }],
}
var obj2 = cloneDeep(obj1)
obj2.hobby[2].say = '你好'
console.log(obj1) // 里面的hobby[2].say = 'hello'
console.log(obj2) // 里面的hobby[2].say = '你好'

节流和防抖

节流与防抖可以对事件的触发进行优化,缓解函数频繁调用

  • 事件触发次数过多,ajax向后台频繁发送数据请求,造成服务器压力
  • 造成浏览器卡顿,影响用户体验

节流

函数节流(throttle),指事件触发后,每过n秒才会响应一次,如果在n秒内多次触发,不会执行代码即不会进行响应。

节流会有规律的执行

原理:

利用闭包变量(保存上一次执行时间),在执行函数前会同时获取当前的时间,将当前时间与保存的上一次执行时间比较,只有当超过规定间隔时间才会执行

<script>
    function throttle(fn, wait) {
    	var lastTime = 0 // 定义初始上一个执行时间
    	return function () {
       		var newTime = Date.now() // 获取当前执行时间
        	// 只有在当前时间超过上一个执行时间 wait 秒后才再次触发事件
        	if (newTime - lastTime >= wait) {
            	fn.apply(this, arguments)
            	lastTime = newTime
        	}
    	}
	}
    document.addEventListener(
        'scroll',
        throttle(function (event) {
            console.log('滚动啦')
            console.log(this)
            console.log(event)
        }, 2000)
    )
</script>

函数节流应用场景

节流的根本目的是为了解决一定时间内事件触发太多次的问题,一般多用在鼠标事件中。

常见的场景:

  • 防止用户频繁点击(onclick)
  • 滚动scroll,连续滚动只执行一次

防抖

函数防抖(debounce),指事件触发后,过n秒后仅执行一次函数,如果在n秒内多次触发则每次都重新等待n秒后在执行

防抖只执行最后一次

原理:

将事件的执行放入n秒的延时器中,在延时时间结束前,每触发一次事件都会清除上一个延时器并产生一个新的延时器(即重置延迟时间)。只有当延时时间结束后才会执行一次。

<script>
    function debounce(fn, wait) {
    	var timer
    	return function () {
       		// 创建变量保存子函数中正确的this与arguments
        	var _this = this
        	var args = arguments
        	// 判断上一次的延时器是否存在,存在则清除上一次延时器,随后生成一个新的延时器(即重置延迟时间)
        	if (timer) {
            	clearTimeout(timer)
        	}
        	timer = setTimeout(function () {
            // 将this指向为正确的this,而不是时间器中嵌套的函数this与arguments
            	fn.apply(_this, args)
        	}, wait)
    	}
	}
    document.getElementById('inp').addEventListener(
        'input',
        debounce(function (event) {
            console.log('按键触发了')
            console.log(this)
            console.log(event)
        }, 500)
    )
</script>

函数的防抖应用场景

防抖的根本目的是在一定的时间内,只执行最后一次触发的事件,一般多用在键盘事件onkeyup、或内容改变事件oninput中

常见的场景:

  • 校验用户名
  • 关键字搜索