JavaScript 函数(闭包、纯函数、高阶函数、防抖与节流)

1,047 阅读6分钟

函数有哪些种类?

  1. 普通函数function foo(){}
  2. 箭头函数const foo = ()=>{}
  3. generatorfunction* foo(){} Generator 函数是 ES6 提供的一种异步编程解决方案
  4. constructor 函数class Foo{ constructor(){...} } 类的构造函数
  5. async 函数async function foo(){} Generator 函数的语法糖

什么是立即执行函数?

通过立即执行函数(IIFE)可以实现块级作用域

(function(){
    //这里是块级作用域,这里面定义的变量外部是无法访问(除非自己return出去)
    var a = 1;
})();

console.log(a); // Uncaught ReferenceError: a is not defined

ES6以后已经有了块级作用域的概念了,因此IIFE已经逐渐退出历史舞台了。

什么是闭包?

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数

var closure = function(){
    var count = 0;
    return function(){
       return count ++;
    }
}
const fn = closure(); 
console.log(fn()); // 0
console.log(fn()); // 1
console.log(fn()); // 2

fn = closure() 相当于 fn = function(){ return count ++; } , fn一直保持着对这个匿名函数的引用,而这个匿名函数又引用着count变量。因此count变量一直存在于执行上下文中,不会消失。

闭包的定义我们已经明白了,那么它可以干什么呢?

闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作

var closure = (function(){
    var foo = "foo";
    return {
        getFoo: function(){
            return foo
        },
        setFoo: function(newFoo){
            return foo = newFoo
        }
    }
})(); 

console.log(closure.getFoo()); // foo
console.log(closure.setFoo("newFoo")); // newFoo
console.log(closure.foo); // 获取不到

什么是纯函数?

  1. 不会产生副作用的函数
  2. 相同的输入,永远会得到相同的输出

这是个纯函数,满足上面两点条件

const pureFn = function(a,b){
    return a + b
}

这个不是存函数,它的副作用就是修改了外部作用域的变量。

let sum = 0;
const plus = (a,b)=>{
  sum = a + b;
  return sum;
}

我们构建一个项目或者说去编写一个组件,往往业务都是非常复杂的,需要定义各种变量,各种对象。如果我们定义的方法大多对外部有副作用的话,容易不可控,也就是说我们容易疏忽细节导致bug。这也就是我们需要尽可能的去编写纯函数的原因。

函数参数是对象

function test(person) {
  person.age = 26
  person = {
    name: 'yyy',
    age: 30
  }
  return person
}
const p1 = {
  name: 'yck',
  age: 25
}
const p2 = test(p1)
console.log(p1) // ->  26 'yck'
console.log(p2) // ->  30 'yyy'

代码解释:

  • 函数传参是传递对象指针的副本,因此函数里面如果修改了对象副本的话,原对象也会跟着修改的
  • 函数里面的person属性被重新赋值一个新对象,因此p2就等于新对象。

箭头函数和普通函数的区别

// 箭头函数
var f = v => v;

// 普通函数
var f = function (v) {
  return v;
};

箭头函数特性:

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。const sortNumbers = (...numbers) => numbers.sort();
  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

call 与 apply 函数

首先call和apply两个函数除了传参方式不同,功能是一样的。

改变 this 指向

call和apply最常见的用途是改变函数内部的this指向

var obj1 = {
    name: 'sven'
};

var obj2 = {
    name: 'anne'
};

window.name = 'window';

var getName = function(){
    alert ( this.name );
};

getName();    // 输出: window
getName.call( obj1 );    // 输出: sven
getName.call( obj2 );    // 输出: anne

借用其他对象的方法

(function(){
    Array.prototype.push.call( arguments, 3 );
    console.log ( arguments );    // 输出[1,2,3]
})( 1, 2 );

Array.prototype.push.call( arguments, 3 ) 相当于push方法中的this被arguments对象替换了,然后返回了一个数组。其本质依然是改变了this指向。只是最后实现的结果看起来像是借用了方法。

再看一个例子:

var A = function( name ){
    this.name = name;
};

var B = function(){
    A.apply( this, arguments );
};

B.prototype.getName = function(){
    return this.name;
};

var b = new B( 'sven' );
console.log( b.getName() );  // 输出: 'sven'

在B构造函数中借用了A构造函数的属性。

高阶函数

高阶函数是指至少满足下列条件之一或同时满足

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

函数作为参数传递

常见的场景就是回调函数

var getUserInfo = function( userId, callback ){
     $.ajax( 'http://xxx.com/getUserInfo?' + userId, function( data ){
        if ( typeof callback === 'function' ){
            callback( data );
        }
    });
}

getUserInfo( 13157, function( data ){
    alert ( data.userName );
});

函数作为返回值输出

相比把函数当作参数传递,函数当作返回值输出的应用场景也许更多,也更能体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。

var isType = function( type ){
    return function( obj ){
        return Object.prototype.toString.call( obj ) === '[object '+ type +']';
    }
};

var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumber = isType( 'Number' );

console.log( isArray( [ 1, 2, 3 ] ) );     // 输出:true

isType -> isString 然后通过 isString 就可以判断是否是字符型

函数即使参数也是返回值

单例模式的实现:

var getSingle = function ( fn ) {
    var ret;
    return function () {
        return ret || ( ret = fn.apply( this, arguments ) );
    };
};

这个高阶函数的例子,既把函数当作参数传递,又让函数执行后返回了另外一个函数。我们可以看看getSingle函数的效果:

var getScript = getSingle(function(){
    return document.createElement( 'script' );
});

var script1 = getScript();
var script2 = getScript();

alert ( script1 === script2 );    // 输出:true

函数节流(throttle)

JavaScript中的函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则我们一般不会遇到跟性能相关的问题。但在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。

window.onresize事件、mousemove事件、上传进度等场景下函数会被频繁调用而影响性能。

函数节流(throttle),在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

var throttle = function ( fn, interval ) {

    var __self = fn,    // 保存需要被延迟执行的函数引用
        timer,      // 定时器
        firstTime = true;    // 是否是第一次调用

    return function () {
        var args = arguments,
            __me = this;

        if ( firstTime ) {    // 如果是第一次调用,不需延迟执行
            __self.apply(__me, args);
            return firstTime = false;
        }

        if ( timer ) {    // 如果定时器还在,说明前一次延迟执行还没有完成
            return false;
        }

        timer = setTimeout(function () {  // 延迟一段时间执行
            clearTimeout(timer);
            timer = null;
            __self.apply(__me, args);

        }, interval || 500 );

    };

};

window.onresize = throttle(function(){
    console.log( 1 );
}, 500 );

函数防抖(debounce)

函数防抖(debounce),防抖的中心思想在于,我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。

// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
var debounce  = function(fn, delay) {
  // 定时器
  let timer = null
  
  // 将debounce处理结果当作函数返回
  return function () {
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments

    // 每次事件被触发时,都去清除之前的旧定时器
    if(timer) {
        clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}