JS递归、闭包、作用域链

递归

常规的递归函数

function func(number) {
    if (number <= 1) {
        return 1;
    }
    else {
        return number * func(number - 1);
    }
}
console.log(func(4));	// 24

这是一个非常常见的递归函数,但是有一种情况会使得此方法出现错误。

var func2 = func;
func = null;
console.log(func2(4));

将递归函数赋值给一个变量,然后把原函数赋值为null,这时候再调用就会报错

image-20200730225021398

这是因为function属于引用类型,赋值时通过指针指向原函数,原函数发生改变,调用时就会出错

使用callee

在上面的情况下,使用arguments.callee就可以解决,arguments.callee是一个指向正在执行函数的指针,将经典递归做以下改动

function func(number) {
    if (number <= 1) {
        return 1;
    }
    else {
        return number * arguments.callee(number - 1);
    }
}
console.log(func(4));	//24

使用指针进行递归,无论怎么调用函数都不会出错,但是,在严格模式下使用arguments.callee会报错

image-20200730225603025

命名函数表达式

以上两种方法都存在这不同程度的缺陷,使用命名函数表达式可以达到指针的效果,亦可以同时运行在严格模式和非严格模式下

var func2 = (function func(number) {
    if (number <= 1) {
        return 1;
    }
    else {
        return number * func(number - 1);
    }
});
var func3 = func2;
func2 = null;
console.log(func3(4));

这时依旧可以正常显示结果

image-20200730230341955

闭包

闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。闭包会发生内存泄漏,每次外部函数执行的时候,外部函数的引用地址不同,都会重新创建一个新的地址。但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。

上面的递归最后一种方法就可以理解为一个闭包

闭包与变量

经典的示例

function newArray() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        }
    }
    return result;
}
var newArr = newArray();

for (var i = 0; i < 10; i++) {
    console.log(newArr[i]());
}
// 10,10,10,10,10,10,10,10,10,10

乍一看好像是for循环中每次循环将0-9赋值给数组的每一项,其实并不是,因为当前的newArray()正在执行,每个函数作用域链都保存着newArray()的活动对象,所以for循环中每次都是引用的同一个变量i,for循环结束之后,i是10,所以每个函数内部的i都是10。

再将返回i的函数包在一个匿名函数中,就可以达到互不影响的效果

function newArray() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function (num: number) {
            return function () {
                return num;
            };
        }(i)
    }
    return result;
}
var newArr = newArray();
for (var i = 0; i < 10; i++) {
    console.log(newArr[i]());
}

结果如下

image-20200731104250987

在for循环中,自调用函数立即执行,获取实时地i赋值给num. 循环结束之后,返回result,在自调用函数执行之后,他的作用域就被销毁了,不会再被其他操作影响

this对象

// 非严格模式
var arg = 'window';
var object = {
    arg: 'object',
    getArg: function () {
        return function () {
            return this.arg;
        };
    }
};
console.log(object.getArg()());	//window

为什么结果会是window呢,来看一下最后一行

// object.getArg()()相当于
var val = object.getArg();
val();

把getArg返回的方法赋值给了val变量,然后执行函数。因为val在全局中,所以获得的arg是全局中的arg
还有一种情况会出现问题

var arg = 'window';
var object = {
    arg: 'object',
    getArg: function () {
           return this.arg;
    }
};
  • object.getArg() // object
  • object.getArg() // object
  • (object.getArg = object.getArg)() // window

第一种方法简单的调用了object的方法;第二种方法引用了object的方法,this对象能够维持;但是第三种是一个赋值语句,赋值语句是有返回值的,也就是等号右边,所以这里引用了方法,但不是object内部而是指向window

image-20200802084112306

从这张图就可以看出赋值语句其实是有返回值的

解决方法

1 传递this指向

var arg = 'window';
var object = {
    arg: 'object',
    getArg: function () {
        var _this = this;
        return function () {
            return _this.arg;
        };
    }
};
console.log(object.getArg()());

将object的this传递到getArg之内,然后使用这个引用值获取arg

2 箭头函数

也就是lambda表达式

var arg: string = 'window';

var object = {
    arg: 'object',
    getArg: function () {
        return () => {
            return this.arg
        }
    }
}
console.log(object.getArg()());

箭头函数的一大特点就是不改变this的指向

内存泄漏

闭包的缺陷在于会造成内存泄露,当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,比如通过node结点给html元素添加点击事件的时候,当事件添加完之后,存储node结点的变量会一直占用内存,造成内存泄漏

var divNode = document.getElementById('box');
divNode.onclick = function () {
    alert('click');
};

存储divNode的内存将会无法释放也无法使用,怎么解决呢

在添加完事件之后将divNode手动赋值为空

var divNode = document.getElementById('box');
divNode.onclick = function () {
    alert('click');
};
divNode = null;

作用域链

作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。

全局作用域

一般全局作用域有以下几种情况

  • 最外层函数和最外层函数外面定义的变量拥有全局作用域
  • 所有未直接定义的变量
  • 所有window对象

函数作用域

声明在函数内部的变量,函数作用域一般只有代码片段中可以访问,比如函数内部

例如

function func1(a) {
    var b = a * 2;
    function func2(c) {
        console.log(a, b, c);
    }
    func2(b * 2);
}
func1(2)

这段代码中存在3个作用域

  • 全局作用域,标识符func1
  • 作用域func1,标识符a,b,func2
  • 作用域func2,标识符c

作用域链

由多级作用域连续引用形成的链式结果,掌管一切变量的使用顺序: 先在局部找,没有,就延作用域向父级作用域找。

可以向上搜索,但不可以向下搜索

这种一层一层的关系,就是 作用域链 。


前端小白