javascript的函數執行上下文及this

Functions and execution contexts in JavaScript

Publicado el 24/2/2011 por

Sergio Cinos Senior Architecture Engineer

Functions are the main building block of JavaScript. Functions define the behaviour of things like closures, ‘this’, global variables vs. local variables... Understanding the functions is the first step to truly understand how JavaScript works.

As we already know, functions can access variables declared ‘outside’ the current function’s scope, global variables, and as well as variables declared inside the function and those passed in as arguments. Also, the variable ‘this’ points to ‘the container object’. All of these form an ‘environment’ for our function that defines which variables are accessible by the function and their values. Some parts of this ‘environment’ are defined when the function is defined and others when the function is called.

Understanding what happens internally when a function is called may be a little difficult the first time, mainly due to the technical details and nomenclature. In order to be clear, some parts of this article are simplifications of the technical explanation. The specific details can be found in section 10.1.6 ofECMA 262 (3rd edition).

When a function is called, an ExecutionContext is created. This context defines a big part of the function’s ‘environment’, so let’s see how this is constructed (the order is important):

  1. The arguments property is created. This is an array-like object with integer keys, each one referencing a value passed into the function call, in that same order. This object also containslength (number of values passed in the function call) and callee (reference to the function being called) properties.
  2. Function’s scope is created, using [[scope]] property and this ExecutionContext. More details on this later.
  3. Variable instantiation takes place now. It has 3 substeps (also in order):
    1. ExecutionContext gets a property for each argument defined in the function signature. If there is a value for that position in the arguments object, the value is assigned to the new created property. Otherwise, the property will have the value undefined.
    2. The function’s body is scanned to detect FunctionDeclarations. Then, those functions are created and assigned as a property to ExecutionContext using defined names.
    3. The function’s body is scanned to detect variable declarations. Those variables are saved as a property in ExecutionContext and initialized as undefined.
  4. The this property is created. Its value depends on how the function was called:
    1. Regular function (myFunction(1,2,3)). The value of this points to the global object (i.e.window).
    2. Object method (myObject.myFunction(1,2,3)). The value of this points to the object containing the function (i.e. the object before the dot). The value is myObject in our example.
    3. Callback for setTimeout() or setInterval(). The value of this points to the global object (i.e.window).
    4. Callback for call() or apply(). The value of this is the first argument of call()/apply().
    5. As constructor (new myFunction(1,2,3)). The value of this is an empty object withmyFunction.prototype as prototype.

Let’s see an example of this process in pseudo-code:

JavaScript code:

function foo (a, b, c) {
	function z(){alert(‘Z!’);}
	var d = 3;
}
foo(‘foo’,’bar’);

ExecutionContext in the foo() call: Step 1arguments is created

ExecutionContext: {
	arguments: {
		0: ‘foo’, 1: ‘bar’,
		length: 2, callee: function() //Points to foo function
	}
}

Step 3a: variable instantiation, arguments

ExecutionContext: {
	arguments: {
		0: ‘foo’, 1: ‘bar’,
		length: 2, callee: function() //Points to foo function
	},
	a: ‘foo’, b: ‘bar’, c: undefined
}

Step 3b: variable instantiation, functions

ExecutionContext: {
	arguments: {
		0: ‘foo’, 1: ‘bar’,
		length: 2, callee: function() //Points to foo function 
	},
	a: ‘foo’, b: ‘bar’, c: undefined,
	z: function() //Created z() function
}

Step 3c: variable instantiation, variables

ExecutionContext: {
	arguments: {
		0: ‘foo’, 1: ‘bar’,
		length: 2, callee: function() //Points to foo function
	},
	a: ‘foo’, b: ‘bar’, c: undefined,
	z: function(), //Created z() function,
	d: undefined
}

Step 4: set this value

ExecutionContext: {
	arguments: {
		0: ‘foo’, 1: ‘bar’,
		length: 2,	callee: function() //Points to foo function
	},
	a: ‘foo’, b: ‘bar’, c: undefined,
	z: function(), //Created z() function,
	d: undefined,
	this: window
}

After the creation of ExecutionContext, the function starts running its code from the first line until it finds a return or the function ends. Every time this code tries to access a variable, it is read from the ExecutionContext object.

In JavaScript, every single instruction is executed in an ExecutionContext. As we have seen, all code within any function will have an ExecutionContext associated, no matter how the function was created or invoked. Therefore, every single statement inside any function is executed in that function’s ExecutionContext. The code that does not belong to any function but the Global code (code executed inline, loaded via <script>, executed through eval()...) is associated to a special context called GlobalExecutionContext. This context works very much like ExecutionContext, but since we do not have function arguments, only step 3) and 4) take place (this points to global object, usually window). In conclusion, every JavaScript statement runs ‘inside’ anExecutionContext.

As the execution of our program goes on, it will ‘jump’ from one function to another (via direct calls, DOM events, timers...). As each function has its own ExecutionContext, these function calls will create a stack of contexts. For example, let’s see the following code.

<script>
	function a() {
		function b() {
			var c = {
				d: function() {
					alert(1);
				}
			};
			c.d();
		}
		b.call({});
	}
	a();
</script>

When the JavaScript engine is about to execute the alert() function, the ExecutionContext stack is:

  1. d() Execution context
  2. b() Execution context
  3. a() Execution context
  4. Global execution context

The most important part of the ExecutionContext stack happens when the function is defined. The key idea to fully understand JavaScript contexts is that every function declaration is executed inside a ExecutionContext (in the previous example, function b() is declared and created inside a()ExeuctionContext). Everytime a function is created, the current ExecutionContext stack is saved in the [[scope]] property of the function itself. All of this happens at function creation. This stack is preserved and tied to the newly created function, even if the original function has finished (this can happen if the original function returns the created function as result, for example).

Now we can explain the step 2) of ExecutionContext creation. At this point, a newExecutionContext stack is created, pushing the function’s ExecutionContext on top of the mentioned [[scope]] property. This stack is also called ‘scope chain’. Note that theExecutionContext stack can be (and usually is) different from the calling stack. The later is defined when the functions are called, and the former when they are defined. For example: function a() calls function b() which calls function c(). This would be the calling stack that can be inspected in any debug tool. However, function c() might have been created inside a function d(). TheExecutionContext related to d() is part of the scope chain, but not of the calling stack.

When the code within the function is looking for a variable, the scope chain is examined. The engine starts searching for it in the first ExecutionContext in the chain. As it is the function’sExecutionContext itself, it corresponds to the functions arguments, declared variables, etc. If it is not found, the engine will then search for the variable in the next ExecutionContext in the stack and so on until it reaches the end of the chain. If it still not found, it returns undefined as the variable value.

And that is all about function internals: just creating ExecutionContexts and stacking them. Let’s sum up the bullet points about functions and execution contexts:

  • The value of this is not coupled to the function nor is a ‘special’ property, but behaves more like a regular argument. It is defined when the function is called, so the same function can be executed with different values for this.
  • arguments is not an array, just a regular object with numbers as property names. So it does not inherit the array methods like push()concat()slice()...
  • Variables are actually defined in step 3c), no matter where they are defined in the function code. However, initialization takes place when the execution flow reaches the instruction where they are initialized. That is why in our example d points to undefined. It will point to 3 when the execution code reaches the second line of the function code.
  • You can call a function before it is defined. It is allowed by step 3b) in ExecutionContext creation (not true for FunctionExpressions)
  • All inner functions declarations are created at the ExecutionContext step. So an unreachable function declaration will be always created. For example:
    function foo() {
    	if (false) {
    		function bar() {alert(1);};
    	}
    	bar();
    }
    

    It will work (working in IE8, Chrome and Safari5, not in Firefox) because bar() is created atExecutionContext, before starting function’s code and evaluating the if.

  • Variables can be hidden. As all the steps take place in order, later steps can overwrite the job done by previous ones. For example, if we define an argument called foo at function signature, and inside that function we declare another function called foo too, the later will ‘hide’ the former when the ExecutionContext is finally created.
  • Closures: a function can access its ‘parent’ function’s variables. When asking for a variable, the value is not found in our current ExecutionContext but on the next context in the stack: the context from our ‘parent’ function. You can even build ‘multilevel’ closures that uses the data from its parent, grandparent... functions.
  • JavaScript has global variables. In this case, the value is found in the last item of the chain, theGlobalExecutionContext (this is the reason why global variable access is slow; the engine must search for the variable in each context in the stack trace before reaching the global context). Also, you can use semi-global variables: if different functions have a common ExecutionContextin their stacks, any variable declared in that common ExecutionContext will be available for all those functions like a global variable.

JavaScript core is actually ExecutionContexts and scope chains, as most of the language featuresraise from the contexts behaviour. If you get used to designing your program as an interaction ofcontexts, your code will be much simpler and more natural. For example, with contexts in mind, amixin-based inheritance system is very easy to implement (as opposed to many other languages).Most of the JavaScript loading libraries rely on context management to load modules withoutpolluting the global context. To sum up, it is by thinking in contexts and scope chains (and not infunctions and/or objects) that JavaScript unleashes its power. Make sure to understand them asdeeply as you can.

Further reading:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章