作用域、作用域连(scope、scope chain)
作用域
作用域分为:函数作用域、全局作用域
注:ES6中的块级作用域,是假的,实际上是函数作用域
函数作用域
js引擎在调用函数时才临时创建的一个作用域对象,里面保存函数的局部变量,而调用完后,函数作用域对象就释放
全局作用域
其实是window的对象。所有全局变量和全局函数都是window对象的成员
作用域链
一个函数把可用的作用域串起来,形成了一个作用域链
程序中任何地方(函数内、全局)去访问一个变量的时候,会沿着作用域链由内往外,如果找到了,立马停止。
变量
JS中变量分为:局部变量、全局变量
局部变量
- 类似
var定义的变量 形参也是局部变量 优点:不会有污染
缺点:不可重用
全局变量
在浏览器中是保存在window对象上的变量
优点:可以重用
缺点:会形成污染
注意
- 作用域和作用域链都是
对象结构 - 只有
函数的{}才会形成作用域 - 不是所有的{}都能形成作用域
- 不是所有的{}内部都是局部变量 比如:
1.对象的{},就不是作用域
2.对象的属性就不是局部变量
var lilei = {
sname: "Li Lei"
}
面临的问题
如果有一个函数,希望可以修改某一个变量。我们知道,变量分为局部变量、全局变量
如果是局部变量,那么每次在执行函数的时候,都会临时创建作用域和撤销。那么这个局部变量就没有办法做到持久化。
如果是全局变量,可以做到持久化。但是如果项目中其他地方有意无意中修改了这个全局变量。那么程序就会出现BUG。
这个时候就需要一个函数,提供一个可以供内部函数修改的一个局部变量
创建闭包三步走
- 创建外层函数
- 创建函数内的变量和返回的内层函数
- 执行外层函数,返回内层函数
// 1.创建外层函数mother
function mother() {
// 外层函数内的变量total
var total = 1000;
// 2.内层函数
return function pay(money) {
total -= money;
console.info(`花费了${money},剩余${total}`);
}
}
// 3.执行外层函数,返回内层函数
var pay = mother();
外层函数返回内层函数的方法(三种)
- 直接return(顺产)(最常用) 三部走中的例子就是最常用的return方法
- 全局变量(剖腹产)
function mother() {
var total = 999;
nAdd = function() { total++ } // 这里的nAdd就是剖腹产,直接挂到了window全局作用域对象上
}
- 将函数包裹在对象或者数组中返回
function mother() {
var total = 0;
arr = []
arr.push(function() {
total++;
console.info(total)
})
}
闭包是如何形成的(一句话概括)
外层函数在执行后,外层函数的作用域对象,被返回的内层函数的作用域链引用着。无法释放,形成了闭包
闭包缺点
作用域无法释放,容易导致内存泄露。
闭包释放
及时把返回的内层函数赋值为null
pay = null;
练习
练习一
function fun() {
var i = 999;
nAdd = function() { i++ } // 注意这是一个内层函数1。剖腹产
return function() { // 内层函数2。顺产
console.info(i)
}
}
var getN = fun();
getN(); // 999
nAdd();
getN(); // 1000
总结:一次生多个孩子,共享一个红包
练习二
function mother() {
var i = 0;
return function() {
i++
console.info(i);
}
}
var get1 = mother();
get1(); // 1
var get2 = mother();
get2(); // 1
get1(); // 2
get2(); // 2
// 这里的mother生了两次
总结:多次生孩子,每次都有各自的红包
练习三
function fun(){ // 妈妈
arr = []; // 不算红包,因为内存函数中没有用到
for (var i = 0; i <3;i++) { // i 是红包。在for循环结束后,i = 3
// 妈妈生成了三个孩子
arr[i] = function (){
console.info(i);
}
}
}
fun();
arr[0](); // 3
arr[1](); // 3
arr[2](); // 3
总结:妈妈(fun函数)一次剖腹产生了3个孩子(arr中保存的三个方法)。包了一个公用的红包,红包里面是3块钱(for循环后,i = 3)
练习四
var name = "window";
var p = {
name: "peter",
getName: function() { // 闭包外层函数
var self = this; // 外层函数内的局部变量。这里p.getName执行时,this指向p
return function(){ // 内层函数
return self.name;
}
}
}
var getName = p.getName();
var _name = getName();
console.info(_name); // peter
练习五
// 每次都会输出第二个参数值
function fun(n, o) {
console.info(o);
return {
fun: function(m) {
return fun(m, n); // 这里执行的是最外层的fun
}
}
}
var a = fun(0); // undefined
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0
var b = fun(0);
b.fun(1) // 0
.fun(2) // 1
.fun(3) // 2
练习六(实现bind)(重要)
题目:用原生的call()模拟实现bind()
// 应该在所有函数的原型对象中添加自定义bind函数
Function.prototype.bind = function(obj, ...rest) {
console.info("调用自定义的bind()");
// 先获取将来获取bind()的.前的原函数,保存在一个局部变量中
var fun = this; // this-> 将来的function jisuan(){ ... }
return function(...values) { // 调用返回的内层函数,等效于调用原函数
fun.call(obj, ...rest, ...values);
}
}
function jisuan(value1, value2) {
console.info(`${this.ename}, ${value1},${value2}`)
}
var lilei = {
ename: "Li Lei"
}
var jisuan2 = jisuan.bind(lilei, "value1");
jisuan2(2)
使用场景
return一个函数
循环赋值
回调函数就是闭包
防抖节流
匿名函数自调
// 没有自调的情况,下面的arr可以被很多地方访问修改
var arr = [];
function add(){
// 操作arr
}
// 使用匿名函数自调
(function() {
// 这里的代码就是上面的代码
// 这里的arr就不会被外面影响到
})();
柯里化
可以连续给一个函数反复传参,函数传入的参数,还可以累积
比如:实现一个add函数,可以接受任何多的参数,返回和值(add(1)(2)(3))
var add = function(){
var sum = 0;
function temp(num){
sum += num;
return temp
}
temp.toString = function() {
return sum.toString();
}
return temp
}
// 注意第一个参数1是add函数的形参,后面的参数才是temp函数的参数
console.info(`${add(1)(2)(3)}`) // 5
其他
当遇到互不干扰的问题时,要想到闭包