首页 > JavaScript中,作用域,作用域链,执行环境分别是什么时候创建好的,以及函数在定义和调用时发生了什么?

JavaScript中,作用域,作用域链,执行环境分别是什么时候创建好的,以及函数在定义和调用时发生了什么?

先抛砖引玉一下:

代码1:

var object = {
    name: "My Object",
    getName: function() {
        return console.log(this.name)//the window
    }
};
(object.getName = object.getName)();//空

代码2:

    var x=10;
    function foo(){
        console.log(x)
    }
    !function(){
        var x=20;
        console.log(x);//20!
        foo();//10!
    }();

    function foo2(){
        var x=30;
        foo();//10;
        !function(){
            console.log(x)//30
        }()
    }
    foo2();

代码3:

var Fn = {};
Fn.method = function(){
    this.name = 'jack';
    function test(){
      console.log(this === window);
    }
    test();
}
Fn.method();//true

问题来了:

1.函数在定义时发生生了什么?
2.函数在不同方式调用时,又发生了什么?
3.作用域,作用域链,执行环境都是在什么时候形成的?
4.如何解释代码2中的结果?
5.函数执行的地方和函数定义的地方有什么样的联系?
6.this指向与作用域,作用域链,执行环境的关系,以及闭包的三个是什么时候创建和有效的?
7.这方面有什么文章或者书籍的章节比较清晰明朗的?

7月1日追加问题

感谢各位大神,现在对整过过程还有点蒙,不知道我理解的对吗,见代码和注释

    var a=1;
    var fn1=function(){
        console.log(this);
    }

    function fn2(arguments){
        var a=1;
        var c=1;
        console.log(a,c);
        function fn1_1(){
            var c1=0;
            console.log(c1);
        }
    }

    function fn3(){
        console.log(this);
        fn2();
    }
    fn3();
/*
    整个解释过程是这样的:

    页面加载;

    创建window全局对象,并生成全局作用域;

    然后生成执行上下文,预解析变量(变量提升),生成全局变量对象;

    然后逐行解析,执行流依次执行每行/块代码;

    直至运行到fn3();

    执行流将fn3压入环境栈中;

    创建fn3的执行环境,创建定义域和定义域链,根据执行上文创建变量对象;

    在创建变量对象的过程中沿定义域链逐级线上搜索变量,并将结果存在函数变量对象中,其中第一活动对象为arguments;

*/

这么多问题,完全可以写一篇博客了,我慢慢写,你慢慢看,可能要分多次写完

先给你一部分看着,下面的我慢慢写。
代码1、3 涉及的是this 的值,可以看这里:http://zonxin.github.io/post/2015/11/javascript-this
代码2 涉及的是闭包,这部懒么,还没写博客,可以先可看这里:https://.com/q/1010000004736461
最近还筹划用 javascript 写一个lisp 解释器,代码初稿写完了,但是博客还没写,里面除了this的问题,其他的问题都能解决。

不同语言实现原理不同,下面尽量解释一下所有支持闭包的语言的通用原理。然后再具体到javascript。
=================分割线======================================

函数定义时候发生了什么

函数其实是一个对象,在这个对象里保存了如下的值:函数形参的名字,当前执行环境,函数体的代码。有些语言支持默认参数和可选参数,所以可能还保存这些值,当然也可能有其他的值。形参的名字,就是一个字符串数组;函数体代码也是字符串,但是一般解析成解释器更喜欢的形式,否则后面每次执行函数都要重新解析;当前执行环境就是当前代码(函数定义所在的执行环境),执行环境中保存了每个变量的值,也保存了上一级执行环境的引用(指针)。其实函数定义也是一句代码,执行到这句代码前,这个函数是不存在的,比如:

function outer(arg) {
   var v;
   function inner(iarg) { return iarg;}
   return arg;
}

在执行outer函数前,其实在任何地方都不存在inner 函数。如果执行两次outer函数,那么就会有两个inner函数,只是他们函数体相同,形参相同。
如果一个 js 文件里只有这几句代码。可以认为这段代码只有一句话,就是定义一个函数。既然是一个函数就要保存:形参的名字,当前执行环境,函数体的代码,他们分别是:形参列表["arg"],全局执行环境,以及函数体(写为字符串吧):"var v;function inner(iarg) { return iarg;};return arg;"。而不存在一个叫做inner的函数,也不存在一个叫做v的变量。

函数调用的时候发生了什么

实际的执行环境是在函数调用的时候创建的,基本过程是:创建执行环境,其上一级执行环境就是上面保存的"当前执行环境";在执行环境中创建所有形参名字的变量,并赋值为对应的实参;把新创建的这个执行环境作为当前执行环境执行函数体的代码。
比如执行outer(1)的时候(我们知道outer函数中保存了三个值:["arg"],全局执行环境,"var v;function inner(iarg) { return iarg;};return arg;"),执行过程是(先不考虑变量声明提升等):创建一个执行环境,其上一级执行环境是全局执行环境(outer函数中保存的那个),在这个环境中创建一个叫做arg的变量,赋值为1,然后执行"var v;function inner(iarg) { return iarg;};return arg;"。
第一句var v;这是一个变量声明,所以就会在当前执行环境中创建一个名字叫做v的变量。然后第二句:function inner(iarg) { return iarg;},这又是一个函数定义,因此编译器就会创建一个函数(对象),里面保存了:["iarg"],这次执行outer创建的执行环境,以及函数体"return iarg;"。

变量的查找规则

当解释器要查找一个变量的时候,其实是递归查找的。首先在当前执行环境中查找这个变量,然后在上一级执行环境中查找,一直查找到全局的执行环境。如下代码

function add(a) {
   var c = a + a;
   function addAA(b) { return c + b;}
   return addAA;
}

然后执行var fn = add(1);fn(10);,通过上面的分析我们知道,add(1)的时候创建了一个执行环境,假设叫做Env1,其上一级执行环境是全局执行环境。在执行环境Env1中有一个变量a,在执行到var c 的时候在执行环境Env1中创建了一个叫做c的变量,然后赋值为a+a也就是2。同时也创建了一个函数addAA,其中保存了:["b"],Env1(当前执行环境), "return c + b;"。当执行这个addAA函数的时候,会创建一个新的执行环境,其上一级执行环境是Env1,然后给形参赋值,然后执行c+b。这句中使用了变量c,解释器就会查找这个变量的值,首先当前执行环境中并没有这个 c (只有变量b),所以就递归的查找到上一级执行环境。于是在add1中查找了变量c,所以这个c就是此次引用的c,其值为2。
当我们把 addAA 作为参数返回,并赋值给 fn 的时候。fn 依旧是一个保存了:["b"],Env1, return c + b;。根据上面函数的执行过程可知,所以执行 fn 和执行 addAA 没有什么区别,因为函数的执行结果仅仅与对象中保存的这三个值有关,而与函数的调用位置没有关系。

总结

函数就是一个对象,只是我们在js代码里没法改变这个对象的内部属性,这个对象是个常数。创建这个对象的方式就是函数声明,我们不能以任何方式修改这个对象的内部属性。把这个对象赋值给不同的变量同样不会改变这个对象的内部属性,显然return 也不能。对这个对象的调用结果仅仅与这个对象的内部属性有关系,而我们又无法改变这个对象的内部属性,因此当一个函数定义之后无论在哪里调用都是"一样"的执行结果。即函数的执行结果仅仅与其定义所在的位置有关。

======================具体到js的函数定义与执行 ===================
其实这一段我真的不想写,因为本质上 js 解释器也只需要执行上面的的几步。或许因为当年设计的不好,或许是为了使用者方便,或许是为了让他更好,我觉得ES中写的过于复杂了。。。。。。所以,尽量简略吧,以ES 5.1为例,而不是不是 ES2015,。。。其实我都想写ES 3。

// to do

附录:lisp 解释器中的部分代码(JS写的),为了方便阅读,有改动。

// 基本数据类型
function LispVal(type,val){ this.type = type; this.value=val;}
LispVal.prototype = { /*略*/};
//函数类型,其他略
function Func(params,body,closure){
    var fn = new LispVal("Function");
    fn.params = params;
    fn.body = body;
    fn.closure = closure;
    return fn;
}

// 执行环境
function Env(closure){ this.env = {};this.closure=closure;}
Env.prototype = {
    constructor:Env,
    // 当前执行环境是否存在一个变量
    isBound: function(varName) { return this.env.hasOwnProperty(varName); },
    // 变量的取值,赋值,定义
    getVar: function(varName) {
        var that = this;
        while(that){
            if(that.isBound(varName)) { 
                return that.env[varName];
            }
            that = that.closure;
        }
        return throwError("Undefined variable " + varName);
    },
    setVar: function(varName,value) {
        var that = this;
        while(that){
            if(that.isBound(varName)) { 
                that.env[varName] = value;
                return value;
            }
            that = that.closure;
        }
        return throwError("Undefined variable " + varName);
    },
    defineVar: function(varName) {
        if(this.isBound(varName)) {
            return throwError(varName + " has been defined!");
        }            
        this.env[varName] = null;
    }
}

// 代码执行
function evaluateCodeWithEnv(env,code){
    // ....
    // 函数声明
    if(isFunctionDeclare){
        var def_var = code.value[1].value[0].value;  // 函数名
        var def_params = code.value[1].value.slice(1); // 函数参数
        var def_body = code.value.slice(2); // 函数体,
        var fn = Func(def_params,def_body,env);
        env.defineVar(der_var)
        return env.setVar(def_var,fn);
    }
    // 函数调用
    if(isFunctionCall){
        // fn 是要调用的
        var realargs = []; // 计算实参。。。略
        var newEnv = new Env(fn.closure);
        var ret;
        for(i=0;i<fn.params.length;i++){
            newEnv.defineVar(fn.params[i]);
            newEnv.setVar(fn.params[i],realargs[i]);
        }
        for(i=0;i<fn.body.length;i++){
            ret = evaluateCodeWithEnv(newEnv,fn.body[i]);
        }
        // Lisp 里面函数的返回值是最后一句代码的返回值。
        return ret;
    }
   //.....
}

你问的所有问题的答案都在这里,最精确最权威的解释:Executable Code and Execution Contexts。这是原始文档,网上所有其他解释只能算二手的。

如果想彻底搞懂,请仔细一读。


写了个简易的近似,模拟自己构造函数(包括作用域,形参数组等)

function myEval(code, that) {
    return new Function(code).call(that);
}

class Fun {
    constructor(scope, parameters, body) {
        this.scope = scope;
        this.parameters = parameters;
        this.body = body;
    }

    apply(that, args) {
        let setupScopeVar = '';
        let scopeKeys = Object.keys(this.scope);
        if (scopeKeys.length > 0)
            setupScopeVar = 'var ' + scopeKeys.map(key => {
                return key + '=' + this.scope[key];
            }).join(',') + ';'

        let setupParam = '';
        if (this.parameters.length > 0)
            setupParam = 'var ' + this.parameters.map((param, i) => {
                if (i < args.length)
                    return param + '=' + args[i];
                else
                    return param;
            }).join(',') + ';'

        let setup = setupScopeVar + setupParam;

        return myEval(setup + this.body, that);
    }
}

let scope = {
    'a': 1
};

let fun = new Fun(scope, ['b', 'c'], 'return a + b + c;');

console.log(fun.apply(null, [2, 3])); // 6

scope.a = 2;

console.log(fun.apply(null, [2, 3])); // 7

1.

函数在定义时,会发生变量提升

例如,

foo();

function foo() {}

会转化为

function foo() {}

foo();

此外,函数的作用域(链)也被创建

例如:


var x = 0;

function foo() {
    var x = 1; // 在 foo 的作用域中定义了 x,所以 foo 的子作用域不能直接访问到全局变量 x
    function bar() {
        x; // 遍历作用域链,在 foo 的作用域中发现了 x,所以 x = 1
    }
}

2.

函数直接调用时,如果在非严格模式,this指针会指向 window,否则为 undefined

当函数以形如 bar.foo() 的形式调用时,this 指针会指向 foo

注意如果中间存在取值操作,则相当于直接调用。
(foo.bar)() 相当于 var bar = bar; bar();

foo.call(that, a, b, c) 让函数就仿佛是以 that.foo(a, b, c) 的形式被调用的,区别在于使用 call 调用时,foo 无需在 that 的原型链上。

foo.apply(that, [a, b, c]) 相当于 foo.call(that, a, b, c)

除此之外还有一种特殊情况

var bar = foo.bind(that); 创建了一个函数 bar,它的 this 指针永远指向 that

箭头函数,如 x => x + 1this 指针永远指向定义箭头函数时定义域中的 this 指针,相当于
foo.bind(this)

3.

ES2015 之前只有函数作用域(链),函数作用域(链)在定义时就形成了

执行环境,顾名思义,在执行时被创建

4.

正确理解 1,2,3 即可解释,不再重复阐述。

5.

正确理解 1,2,3 即可解释,不再重复阐述。

6.

正确理解 1,2,3 即可解释,不再重复阐述。

7.

参见参考资料

参考资料

ES2015 Language Specification

【热门文章】
【热门文章】