什么是闭包(closure),为什么要用它?

ECMAScript中,闭包指的是:

闭包是指那些能够访问自由变量的函数。

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:
    • 即使创建它的上下文已s经销毁,它仍然存在(比如,内部函数从父函数中返回)
    • 在代码中引用了自由变量

闭包的作用

闭包的特点很鲜明,闭包内,变量无法释放,无法被直接访问;闭包可以被延迟执行。所以可以用它来做一些事情:

  • 管理私有变量和私有方法,将对变量(状态)的变化封装在安全的环境中
  • 将代码封装成一个闭包形式,等待时机成熟的时候再使用,比如实现柯里化和反柯里化
  • 需要注意的,由于闭包内的部分资源无法自动释放,容易造成内存泄露

谈谈call、apply、bind

  • call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法;
  • apply与call方法类似,区别在于apply第二个参数是数组,而call则是单个的参数列表。
  • bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// call模拟实现
Function.prototype.call2 = function(context, ...args) {
var context = context || window;
context.fn = this;

var result = context.fn(...args);
delete context.fn;
return result;
}

// apply模拟实现
Function.prototype.apply2 = function (context, arr) {
var context = Object(context) || window;
context.fn = this;

var result;
if (!arr) {
result = context.fn();
}
else {
result = context.fn(...arr);
}

delete context.fn
return result;
}

// bind模拟实现
Function.prototype.bind2 = function (context) {

if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

var self = this;
var args = Array.prototype.slice.call(arguments, 1);

var fNOP = function () {};

var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}

fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}

说说节流和防抖

在前端开发中会遇到一些频繁的事件触发,比如:

  • window 的 resize、scroll
  • mousedown、mousemove
  • keyup、keydown

限制事件的频繁触发的2种解决方案:

  • debounce防抖
  • throttle节流

debounce防抖

原理:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// @immediate 是否立刻执行
function debounce(func, wait, immediate) {

var timeout, result;

var debounced = function () {
var context = this;
var args = arguments;

if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};

debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};

return debounced;
}

throttle节流

原理:如果你持续触发事件,每隔一段时间,只执行一次事件。
根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
两种主流的实现方式:

  • 使用时间戳

    • 使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
  • 使用定时器

    • 当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 使用时间戳
function throttle(func, wait) {
var context, args;
var previous = 0;

return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}

// 使用定时器
function throttle(func, wait) {
var timeout;
var previous = 0;

return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}

}
}

什么是构造函数?与普通函数有什么区别?

构造函数,就是专门用来生成实例对象的函数。

构造函数与普通函数的区别

  • 构造函数使用new关键字调用;普通函数不用new关键字调用;
  • 构造函数首字母建议大写;普通函数首字母建议小写;
  • 构造函数默认不用return返回值;普通函数一般都有return返回值;
    • 构造函数会默认返回this,也就是新的实例对象;
    • 普通函数如果没有return值的话,返回undefined;
    • 构造函数如果使用了return,那返回值会根据return值的类型而有所不同;

通过new创建一个对象的时候,函数内部有哪些改变

1
2
3
4
5
6
7
8
9
10
11
12
13
function NEW() {
// 获取参数
let func = arguments[0];
let paras = [].slice.call(arguments, 1);
let o = Object.create(func.prototype);
let res = func.call(o, ...paras);

if (typeof res === 'object' && res != null) {
return res;
} else {
return o;
}
}

谈谈高阶函数、函数柯里化、偏函数

高阶函数

概念

  • 高阶函数是对其他函数进行操作的函数,操作可以是将它们作为参数,或者是返回它们。 简单来说,高阶函数是一个接收函数作为参数或将函数作为输出返回的函数。
  • 例如,Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce 是语言中内置的一些高阶函数。

函数柯里化

概念:

  • 柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
  • 柯里化是为函数式而生的东西。

柯里化的使用场景:

  • 参数复用

    • 固定不变的参数,实现参数复用是柯里化的主要用途之一。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      function curriedAdd(x) {
      return function(x, y) {
      return x + y;
      }
      }

      var increment = curriedAdd(1);
      console.log(increment(2)) // 3
      console.log(increment(10)) // 11
  • 柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var overtime = (function() {
    var args = [];

    return function() {
    if(arguments.length === 0) {
    var time = 0;
    for (var i = 0, l = args.length; i < l; i++) {
    time += args[i];
    }
    return time;
    }else {
    [].push.apply(args, arguments);
    }
    }
    })();

    overtime(3.5); // 第一天
    overtime(4.5); // 第二天
    overtime(2.1); // 第三天
    console.log( overtime() ); // 10.1
  • 提高通用性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    function currying(fn) {
    var slice = Array.prototype.slice,
    __args = slice.call(arguments, 1);
    return function () {
    var __inargs = slice.call(arguments);
    return fn.apply(null, __args.concat(__inargs));
    };
    }

    function square(i) {
    return i * i;
    }

    function double(i) {
    return i *= 2;
    }

    function map(handeler, list) {
    return list.map(handeler);
    }

    var mapSQ = currying(map, square);
    mapSQ([1, 2, 3, 4, 5]); //[1, 4, 9, 16, 25]


    var mapDB = currying(map, double);
    mapDB([1, 2, 3, 4, 5]); //[2, 4, 6, 8, 10]

关于柯里化性能

  • 柯里化函数相比 bind 函数,其原理相似,但是性能相差巨大,其原因是 bind 由浏览器实现,运行效率有加成。
  • 柯里化 在 JavaScript 中是“低性能”的,但是这些性能在绝大多数场景,是可以忽略的。

柯里化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function curry(fn, args) {
var length = fn.length;

args = args || [];

return function() {

var _args = args.slice(0),

arg, i;

for (i = 0; i < arguments.length; i++) {

arg = arguments[i];

_args.push(arg);

}
if (_args.length < length) {
return curry.call(this, fn, _args);
}else {
return fn.apply(this, _args);
}
}
}

偏函数

概念:偏函数也可叫局部应用是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

偏函数实现

1
2
3
4
5
function partial(func, ...argsBound) {
return function(...args) {
return func.call(this, ...argsBound, ...args);
};
}

函数式编程

研读中。。。
JS 函数式编程指南