Javascript基础-作用域和闭包

理解词法作用域和动态作用域

动态作用域不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。
作用域链式基于调用栈的,而不是代码中的作用域嵌套。

需要明确的是,JavaScript并不具有动态作用域。它只有词法作用域,简单明了。但是this机制某种程度上很像动态作用域。

1
2
3
4
5
6
7
8
9
function foo(){
console.log(a);//2
}
function bar(){
var a=3;
foo();
}
var a=2;
bar();

理解 JavaScript 的作用域和作用域链

作用域链的定义:函数在调用参数时会从函数内部到函数外部逐个”搜索“参数,一直找到参数为止,如果没有声明就返回null,声明了没有赋值就返回undefined,就像沿着一条链子一样去搜索,这就是作用域的链式调用。

javascrip的全局变量的作用域是全局的,在代码的任何地方都是有定义的。函数的参数和局部变量只在函数体内有定义。在函数内部如果调用一个变量,就会发生上述的作用域链式调用的过程。

this的原理以及几种不同使用场景的取值

1.作为对象方法调用

1
2
3
4
5
6
7
var test = {
a:0,
b:0
get:function(){
return this.a;
}
}

2.作为函数调用
函数也可以直接被调用,此时 this 绑定到全局对象。在浏览器中,window 就是该全局对象。比如下面的例子:函数被调用时,this 被绑定到全局对象,接下来执行赋值语句,相当于隐式的声明了一个全局变量,这显然不是调用者希望的。

3.作为构造函数调用
javaScript 支持面向对象式编程,与主流的面向对象式编程语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。相应的,JavaScript 中的构造函数也很特殊,如果不使用 new调用,则和普通函数一样。作为又一项约定俗成的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。如果调用正确,this 绑定到新创建的对象上。

1
2
3
4
function Point(x, y){ 
this.x = x;
this.y = y;
}

4.在call或者apply,bind中调用
让我们再一次重申,在 JavaScript 中函数也是对象,对象则有方法,applycall 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。

1
2
3
4
5
6
7
8
9
10
11
12
function Point(x, y){ 
this.x = x;
this.y = y;
this.moveTo = function(x, y){
this.x = x;
this.y = y;
}
}
var p1 = new Point(0, 0);
var p2 = {x: 0, y: 0};
p1.moveTo(1, 1);
p1.moveTo.apply(p2, [10, 10])

闭包的实现原理和作用,可以列举几个开发中闭包的实际应用

原理:闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

作用:闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

应用:1. 匿名自执行函数 2. 结果缓存 3. 封装局部变量

理解堆栈溢出和内存泄漏的原理,如何防止

1、内存泄露:是指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存,内存泄露过多的话,就会导致后面的程序申请不到内存。因此内存泄露会导致内部内存溢出

2、堆栈溢出:由于过多的函数调用,导致调用堆栈无法容纳这些调用的返回地址,一般在递归中产生。堆栈溢出很可能由无限递归(Infinite recursion)产生,但也可能仅仅是过多的堆栈层级

3、在一些编程软件中,比如c语言中,需要使用malloc来申请内存空间,再使用free释放掉,需要手动清除。而js中是有自己的垃圾回收机制的,一般常用的垃圾收集方法就是标记清除。

标记清除法:在一个变量进入执行环境后就给它添加一个标记:进入环境,进入环境的变量不会被释放,因为只要“执行流”进入响应的环境,就可能用到他们。当变量离开环境后,则将其标记为“离开环境”。

4、常见的内存泄露的原因:

1
2
3
全局变量引起的内存泄露
闭包
没有被清除的计时器

5、解决方法:

1
2
3
减少不必要的全局变量
减少闭包的使用(因为闭包会导致内存泄露)
避免死循环的发生

如何处理循环的异步操作

在实际开发中,异步总是不可逃避的一个问题,尤其是Node.js端对于数据库的操作涉及大量的异步,同时循环又是不可避免的,想象一下一次一个数据组的存储数据库就是一个典型的循环异步操作,而在循环之后进行查询的话就需要确保之前的数据组已经全部存储在了数据库中。可以得到关于循环的异步操作主要有两个问题:
1.如何确保循环的所有异步操作完成之后执行某个其他操作
2.循环中的下一步操作依赖于前一步的操作,如何解决

———————分割线———————-
方法一:设置一个flag,在每个异步操作中对flag进行检测

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
var func1 = function(callback){
setTimeout(function(){
console.log('foo');
typeof(callback) !== 'function' || callback();
}, 499);
};

var func2 = function(callback){
setTimeout(function(){
console.log('bar');
typeof(callback) !== 'function' || callback();
}, 500);
};

var func3 = function(callback){
setTimeout(function(){
console.log('foobar');
typeof(callback) !== 'function' || callback();
}, 501);
};
var func_arr = [func1, func2, func3];
let len = func_arr.length;
let flag = 0;
for(let i = 0; i < len; i++) {
func_arr[i](function(){
flag++;
if(flag === len) {
console.log('job finished');
}
})

}

方法二:将所有的循环放在一个promise中,使用then处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// promisify those callback functions
var promisify = function(func){
return function(){
return new Promise(function(resolve){
func(resolve);
});
}
}

// array can be infinitely long
var func_arr = [promisify(func1), promisify(func2), promisify(func3)];

func_arr.reduce(function(cur, next) {
return cur.then(next);
}, Promise.resolve()).then(function() {
console.log('job finished');
});

方法三:使用递归,在异步操作完成之后调用下一次异步操作

1
2
3
4
5
6
7
8
9
10
11
12
13
function loop(i) {
if (i != func_arr.length) {
return func_arr[i]()
.then(function() {
return loop(i+1);
});
}
return Promise.resolve(i);
}

loop(0).then(function(loop_times){
console.log('job finished');
});

方法四:使用async和await

1
2
3
4
5
async function loop() {
for(let i = 0; i < len; i++) {
await func_arr[i]();
}
}