关于闭包对JavaScript、Python及Go的执行环境及作用域的理解

Python在名字查找时遵循LEGB作用域顺序的名字查找规则,Python中的函数是作为对象提供给用户的,可以给Python函数添加属性。

def func1():        #函数定义
    var1="heihei"
    def func2():
        return var1
    return func2

var1="haha"
print(func1()()) #打印"heihei",此时对func1名字查找,
#在当前全局作用域(环境)对象中找到了func1函数对象,调用func1(),
返回func1的Enclosed作用域(环境)对象中的函数对象func2,
调用func2(),func2是Enclosed作用域中的函数对象,按照LEGB
顺序在Enclosed作用域中找到变量var1,返回

作用域虽然可以理解成查找起始点往上的上文代码的一个"区域",但这样理解是欠缺并且不充分的,看下面:

class Baseclass(object):
    pass
def Person(name):
    obj=Baseclass()
    def sayName():
        return name
    obj.sayName=sayName
    return obj

person1=Person("haypin")
person2=Person("Nicholas")
print(person1.sayName()) #"haypin"
print(person2.sayName()) #"Nicholas"

在全局作用域定义了类Baseclass和函数Person,函数Person内定义了Baseclass类的一个实例对象obj和嵌套函数sayName,并将这个嵌套函数赋给对象obj,最后返回对象obj的引用给全局变量person1。

如果将作用域当作代码中的一个区域来理解,那么:调用Person函数返回第一个Baseclass实例person1时“在Person函数的Local作用域生成变量name指向字符串"haypin"”,第二次调用Person函数时“在Person函数的Local作用域(与上同一个作用域)修改变量name指向字符串"Nicholas"”,然后执行person1.sayName()会从嵌套函数sayName的Local作用域开始查找名字name,那此时应该查找到Person函数的Enclosed作用域的name="Nicholas"不是么,但显然找到的是第一次调用Person函数时传入的"haypin"。

显然,作用域不能理解为代码中自查找处向上的上文环境一个区域,比如上面第二次调用函数Person并没有覆盖函数Person的Local作用域中变量name。

这样理解:作用域的作用就是顺着作用域链进行名字查找,作用域中有什么?有这些变量的名字呗,变量的名字就是作用域的属性,那么作用域就是个对象,作用域对象的属性就是变量或者变量名,而所有对象都在其定义处的作用域对象上(这会体现在名字查找过程中)!比如,在Local作用域定义了一个函数对象,那该函数对象就在这个Local作用域对象中。而这个作用域对象它不是惟一的,对上面前后两次调用Person函数,两次生成的Person函数的Local作用域对象是独立的,这才能解释person1.sayName()返回的是生成person1时传递给Person函数的name="haypin",而不会被第二次调用Person函数传递的name="Nicholas"给覆盖掉!

所以,上面第一次调用Person函数时形成了全局作用域对象A——Person函数Local作用域对象B——sayName函数Local作用域对象C,并且由于作用域对象C中的函数对象sayName赋给了作用域对象B中的Object对象obj,而obj又被返回给了全局作用域的变量person1,从而作用域对象B与C分别为obj对象与sayName函数对象保留着,就是说sayName函数中进行名字查找时会按照C-B-A的作用域对象链顺序查找对象。如果某个作用域对象中定义的对象没有被外层作用域对象中的对象引用(没有返回),那在这个作用域执行结束后,这个作用域对象就会被析构,因为外部无法访问这个作用域中的任何内容!

第二次调用Person函数时形成了全局作用域对象A(其实相较上面A动态增加了变量person1,但仍是同一个作用域对象)——Person函数Local作用域对象D(与上面B相互独立)——sayName函数Local作用域对象E(与上面C相互独立),同样,作用域对象D、E分别为再次定义的对象obj、再次定义的函数对象sayName而保留。

从而,在全局作用域执行person1.sayName()并名字查找name时,当然是从person1.sayName函数对象所在作用域对象C出发,沿着作用域对象链C-B-A上溯查找并在作用域对象B中找到name=“haypin",输出打印。同理,在执行person2.sayName()并名字查找name时,会按照E-D-A上溯查找并在作用域对象D中周到name="Nicholas"。

JavaSript和Python同为动态类型语言,当然,JavaScript最显著的特点是它没有类的概念与语法,代替以构造函数来生成实例对象、原型对象prototype链来提供类属性、类方法的继承等实现。

《JavaScript高级程序设计》(第3版)4.2执行环境及作用域:

函数每被调用执行一次就生成一个它的作用域对象。同一个函数,不同函数对象副本被调用,会生成不同的作用域对象。每个函数对象副本被执行(函数表达式总是调用Function()构造函数生成函数对象)的同时为其生成作用域对象链。函数对象副本执行结束并被析构(失去引用)的同时销毁其自身作用域对象。


//对象总是存在于某个作用域对象中并记录声明顺序(形成上下文环境,先声明后引用,同名替换),随着函数对象
//的执行动态地构建函数对象自己的作用域对象,只有函数和类会创建新的作用域,并且函数每次被
//调用所生成的作用域对象是不同的(因为实参可能不同)
//Global作用域
function Person(name,age){
  //Enclosed作用域
  var o=new Object();
  o.age=age;
  o.sayName=function(){ //person1.sayName、person2.sayName引用的对象实体在Person作用域
    //Local作用域
    // document.write(name);
    return name;
  };
  return o;
}
var person1=Person("haypin",27);
//执行全局作用域对象中的Person函数对象,动态生成Person函数对象的Enclosed作用域对象E及链E-G进行名字查找,调用结束后间接返回了匿名函数对象,匿名函数对象若被调用则其作用域对象链将引用E-G,因此作用域对象E将被保留

var person2=Person("Nicholas",28);
//再次执行全局作用域对象中的Person函数对象,动态生成Person函数对象的Enclosed作用域对象E2及链E2-G+进行名字查找,调用结束后间接返回了匿名函数对象,匿名函数对象若被调用则其作用域对象链将引用E2-G+,因此作用域对象E2将被保留

document.write(person1.sayName(),"_24<br>");  //haypin
//执行作用域对象E中的匿名函数对象,动态生成匿名函数的Local作用域对象L并形成链L-E-G+进行名字查找,找到E作用域对象中的name="haypin",调用结束,匿名函数没有再返回嵌套函数,销毁此次Local作用域L
document.write(person1.sayName(),"_25<br>");  //haypin
//再次执行作用域对象E中的匿名函数对象,动态生成匿名函数的Local作用域对象L2并形成链L2-E-G+进行名字查找,找到E作用域对象中的name="haypin",调用结束,匿名函数没有再返回嵌套函数,销毁此次Local作用域L2

document.write(person2.sayName(),"_26<br>");  //Nicholas
//调用作用域对象E2中的匿名函数对象,动态生成匿名函数的Local作用域对象L3并形成链L3-E2-G+进行名字查找,找到E2作用域对象的name="Nicholas",调用结束,匿名函数没有再返回嵌套函数,销毁此次Local作用域L3
document.write(person2.sayName(),"_27<br>");  //Nicholas
//再次调用作用域对象E2中的匿名函数对象,动态生成匿名函数的Local作用域对象L4并形成链L4-E2-G+进行名字查找,找到E2作用域对象的name="Nicholas",调用结束,匿名函数没有再返回嵌套函数,销毁此次Local作用域L4

var name="heihei";
person1.sayNameG=function(){return name;} //person1.sayNameG引用的函数对象在全局作用域对象中
document.write(person1.sayNameG(),"_28<br>"); //heihei
//调用全局作用域对象G+中的匿名函数对象,动态生成匿名函数的Local作用域对象L5并形成链L5-G+进行名字查找,找到G+作用域对象中的name="heihei",调用结束,匿名函数没有再返回嵌套函数,销毁此次Local作用域L5

“执行函数的同时动态生成函数对象的作用域对象,连接到该函数对象的作用域对象链的尾端用于名字查找;解析到一个函数对象时为该函数对象生成外层作用域对象链,如果这个函数对象有被直接或间接返回,则其作用域对象链链上的外层作用域对象将可能被引用而得以保留不被销毁”,这在“一切皆对象,函数对象、类对象、方法对象都是对象”的Python中也是一样的:

def createFunctions():
    result=[]
    for i in range(5):
        def functionO(num):
            def functionI():
                return num
            return functionI
        result+=[functionO(i)]
    return result

result=createFunctions()
#print(type(result)) #NoneType
for obj in result:
    print(obj())
 haypin@MBP  ~/VScode/poll0907  python3.8 1106.py
0
1
2
3
4
 haypin@MBP  ~/VScode/poll0907 
def createFunctions():
    result=[]
    for i in range(5):
        def functionI():
            return i
        result+=[functionI]
    return result

result=createFunctions()
for obj in result:
    print(obj())
haypin@ubt:~/VScode_sl$ python3.8 1109.py
4
4
4
4
4
haypin@ubt:~/VScode_sl$

补充:

Go同样支持闭包:

package main
import(
    "fmt"
)
//Global
func FuncOut(para string) func() string { //函数每被执行一次就会生成一个函数的作用域对象
	//Enclosed
	return func() string { //函数被定义在某个作用域对象(在此为嵌套函数FuncOut()的作用域对象)中
		//Local			//,函数执行时的名字查找就是从定义所在的作用域对象上溯查找
		return para
	}
	// return FuncIn	//变量是存在于某个作用域对象(在此为嵌套函数FuncOut()的作用域对象)中的,
}
func main(){
    var FuncIn1 = FuncOut("嘿嘿")                     //第一次调用FuncOut()生成的Enclosed作用域对象中para变量的值为"嘿嘿"
	var FuncIn2 = FuncOut("哈哈")                     //第二次调用FuncOut()生成的Enclosed作用域对象中para变量的值为"哈哈"
	fmt.Println("验证Go闭包同样遵循作用域对象链的查找规则", FuncIn1()) //"嘿嘿"
	FuncIn2()
}

另外值得注意的是,Go的包级作用域类似于C++的类内作用域,不受"先声明后使用"的作用域规则约束。

补充:Go的闭包,《The Go Programming Language》:

定义在函数作用域对象中的变量、常量,每当函数执行一次就会为这些变量、常量申请内存空间(并执行初始化,比如Go语言),这些在函数帧中定义的变量、常量在函数每次调用时都会生成一个,具有各自的内存空间。而函数作用域中引用的不在函数中定义的变量|常量将会按照作用域对象链名字查找,找到最近的名字,然后引用那个位于上层函数帧内存空间中的变量,如此,当前函数每次执行都引用上层作用域对象中定义的那个变量。

//Global

int nCount=5;

void Recurs(){

    //Local

    // cout<<nCount<<" "; //5 4 3 2 1 0

    if(nCount-->0) Recurs();

    cout<<nCount<<" "; //-1 -1 -1 -1 -1 -1

}

int main(int argc,char* argv[]){
    Recurs();

    cout<<"\n";

}

使用闭包而不是分离出一个函数,除了使语法简介,还避免了传参,看一段Go代码的闭包:

func main() {
    naturals := make(chan int)
    squares := make(chan int)
    // Counter
    go func() {
        for x := 0; ; x++ {
        naturals <- x
        }
    }()
    // Squarer
    go func() {
        for {
            x := <-naturals
            squares <- x * x
        }
    }()
    // Printer (in main goroutine)
    for {
        fmt.Println(<-squares)
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值