javascript面向对象 - JavaScript中,作用域,作用域链,执行环境分别是什么时候创建好的,以及函数在定义和调用时发生了什么?
怪我咯
怪我咯 2017-04-11 11:18:56
0
4
361

先抛砖引玉一下:

代码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;

*/
怪我咯
怪我咯

走同样的路,发现不同的人生

reply all(4)
黄舟

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

先给你一部分看着,下面的我慢慢写。
代码1、3 涉及的是this 的值,可以看这里:http://zonxin.github.io/post/2015/11/javascript-this
代码2 涉及的是闭包,这部懒么,还没写博客,可以先可看这里:https://segmentfault.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;
    }
   //.....
}
巴扎黑

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

黄舟

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

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
迷茫

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

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

Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!