1 变量
1.1 基本类型和引用类型的值
var num1 = 5;
var num2 = num1;
num1 ++;
console.log(num2);//5
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Dendi";
console.log(obj2.name);//Dendi
同样是复制,num1和num2的值完全独立,而obj1和obj2却是指向同一个对象。
1.2 传递参数
ECMScript中所有函数的参数都是按值传递的!把函数外部的值赋值给函数内部的参数(arguments),如同把一个变量复制到另一个变量一样。
function addTen(num){
return num + 10;
}
var num1 = 5;
addTen(num1);
console.log(num1);//5
function setName(obj){
obj.name = "Dendi";
}
var person= new Object();
setName(person);
console.log(person.name);//Dendi
上述代码中,num1作为参数传递给函数,在函数内部,参数num的值被加了10,但不会影响到外面num1变量,这个很好理解,也说明了基本类型的值是按值传递的。
但是对于第二点,为什么函数内部obj添加了name属性后,函数外面的person也会改变呢?很多人就认为这个obj参数是按引用传递了。
再看两段代码:
function setName(obj){
obj.name = "Dendi";
obj = new Object();
obj.name = "Miracle";
}
var person = new Object();
setName(person);
console.log(person.name);//Dendi
var person1 = new Object();
var person2 = person1;
person2.name = "Dendi";
person2 = new Object();
person2.name = "Miracle";
console.log(person1.name);//Dendi
上面两段代码的实现效果是一样的。说明person->obj的传递方式和person1->person2的方式一致,否则结果就应该和下面的一样了。
var person = new Object();
person.name = "Dendi";
person = new Object();
person.name = "Miracle";
console.log(person.name);//Miracle
现在我知道了,函数的参数传递和变量传递一样,而且这个外面的person和里面的obj访问的是同一个对象就好了。
至于上面讨论的问题:参数按什么传递,Does it matter?
2 执行环境和变量对象
执行环境(execution context,简称context)是一个抽象的概念。
每个context里有一个与之关联的变量对象,context里面所有定义的变量和函数都放在这个对象里面。
ECMScript执行机制:每个函数都有一个自己的context,当代码执行到一个函数里时,函数的context会被推入到一个堆栈里面,堆栈底部永远都是全局上下文(global context),而当函数执行完时,堆栈将其context弹出。
JS在运行context
代码时,分为两个阶段
- 进入context
- 执行代码
如果这个context是函数,我们把活动对象作为变量对象,活动对象一开始只包含arguments对象。(全局上下文中的变量对象和函数上下文中的变量对象的区别!!)
进入context时,活动对象包括:
- 函数的形参
- 函数声明
#如果变量对象已经存在相同名称的属性,则完全替换这个属性 - 变量声明
#由名称和对应值(undefined)组成
#如果变量名称跟已经声明的形参或函数相同,则变量声明不会替换它
有了这个概念,下面的结果就很好理解了
alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20
//进入上下文阶段:变量声明没有影响同名的function的值
VO= {
x: <reference to FunctionDeclaration>
};
//执行代码时
VO['x'] = 10;
VO['x'] = 20;
3 作用域链
定义:
作用域链这个概念很好理解,其目的是为了保证在内部函数的执行环境中,能对变量对象进行有序的访问,也用于查找变量。
函数被调用时,会创一个执行环境以及相应的作用域链。
作用域链的顶端始终是当前执行的代码所在的环境的变量对象。它的下一个来自外部环境(父函数)的变量对象,再下一个来自更外部的环境的变量对象,一直延续到全局环境的变量对象。
函数的生命周期
- 函数创建
创建函数时,会预先创建一个包含父-变量对象函数的作用域链,并保存在[[scope]]属性中。而这个[[scope]]在函数创建时被存储,静态的(不变的)。 - 函数激活
函数调用时,执行环境被创建,然后复制函数的[[scope]]属性中的作用域链,同时有一个活动对象被创建,并推入作用域链中。
code:
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo(); // 60
对此,分析如下:
1.全局环境的变量对象:
globalContext.VO = {
x:10,
foo:<< function >>
}
2.foo创建
foo[[scope]]={
globalContext.VO
}
3.foo激活
//活动对象
fooContext.AO = {
y = 20;
bar:<< function >>
}
//作用域
fooContext.scope = fooContext.AO + foo[[scope]]);
//作用域链
fooContext.scopeChain = [
fooContext.AO,
globalContext.VO
]
4.bar被创建
bar[[scope]]=[
fooContext.AO
globalContext.VO
];
5.bar被激活
//活动对象
barContext.AO = {
z:30
}
//bar作用域
barContext.scope = barContext.AO + bar[[scope]];
//bar作用域链
barContext.scopeChain = [
barContext.AO,
fooContext.AO,
globalContext.VO
];
4 闭包
自动垃圾回收
ECMScript使用自动垃圾回收。这一规范没有定义细节,留给开发者整理,目前一些已知的实现里,垃圾回收的操作优先级很低。但一般来说,如果对象变得不可引用了(没有剩余的引用给执行代码访问),它就可以被垃圾回收并且在未来的某一时刻被摧毁,它所消耗的资源也会被释放还给系统。
在退出执行环境时,这一现象经常发生。作用域链、活动/变量对象以及任何在执行环境里创建的对象,包括函数对象,都会变得不可获得并且会被垃圾回收。
创建闭包
function exampleClosureForm(arg1, arg2){
var localVar = 8;
function exampleReturned(innerArg){
return ((arg1 + arg2)/(innerArg + localVar));
}
/* return a reference to the inner function defined as -
exampleReturned -:-
*/
return exampleReturned;
}
var globalVar = exampleClosureForm(2, 4);
globalVar (8);
globalVar = null;//解除对exampleReturned函数的引用,释放内存。
上面一章说道,函数内部定义的函数,它的作用域链里会包含外部函数的活动对象。
因此,当exampleReturned被返回后,它的作用域链会包含exampleClosureForm的活动对象和全局变量。这样,exampleReturned内部可以访问exampleClosureForm中定义的所有变量。而且,exampleClosureForm函数结束后,其活动对象也不会销毁。因为exampleReturned的作用域仍在引用他们。
正是由于“闭包”会携带包含它的函数的作用域,因此它会比其他函数占用的内存过多。
一个[[scope]]
在ECMAScript中,同一个父上下文中创建的闭包是共用一个[[Scope]]属性的
任何一个闭包里面改变了scope的属性,也会影响到其他闭包函数的取值。
eg.
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
alert(k);
};
}
data[0](); // 3, 而不是0
data[1](); // 3, 而不是1
data[2](); // 3, 而不是2
5 关于this对象
this取决于调用函数的方式!!
var name = "window"
var foo = {
name : "foo",
bar:function(){
alert(this.name);
alert(this === foo);
}
};
foo.bar();//foo true
(foo.bar = foo.bar)();//window false;
那调用函数的方式如何影响this值?我们需要引入一个概念:引用类型(reference type)
内部的引用类型并不是一个数据类型,一个引用类型其实是一个对对象属性的引用。它包含两个组件,base object和property name。
//两个抽象方法:
referenceType:{
GetBase(V):返回base object
GetPropertyName(V) :返回属性名
}
引用类型的值有两种情况:
- 处理标示符
标识符是变量名,函数名,函数参数名、全局对象中未识别的属性名。
- 处理属性访问器
foo.bar();
foo['bar']();
那么引用类型的值和this如何关联呢?这里有一个规则:
如果调用括号()的左边是引用类型的值,那么this将设为引用类型值的base对象(base object)
其他情况下(与引用类型不同的任何其它属性),这个值为null。
eg.引用类型
var bar = 0;
function foo() {
alert(this.bar);
}
var x = {bar: 10};
var y = {bar: 20};
x.test = foo;
y.test = foo;
foo();//0
x.test(); //10
y.test(); // 20
/*
fooReference = {
base:global,
propertyName:'test'
};
testReference1 = {
base:x,
propertyName:'test'
};
testReference2 = {
base:y,
propertyName:'test'
};
*/
当调用括号的左边不是引用类型而是其它类型,那么这个值会自动设置为null,结果就是全局对象了。
看几个例子:
var foo = {
bar: function () {
alert(this);
}
};
foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo
(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global?
结合上面说的引用类型
来看,第一个例子很好理解,关键是后面4个例子,为了看懂这些东西,博主搜了好久的资料,整理如下:
comma operator 和 simple assignment,都依赖于调用内部的GetValue方法。如果GetValue的参数不是引用而被直接返回的话,就会导致base object找不到了。如下代码所示:
// pseudo-code
GetValue(V) :
if (Type(V) != Reference) return V;
baseObject = GetBase(V); // in your example foo
if (baseObject === null) throw ReferenceError;
return baseObject.[[Get]](GetPropertyName(V));
// equivalent to baseObject[v.PropertyName];
而理解(foo.bar = foo.bar)() 和 foo.bar()不同的关键在于Simple Assignment 操作:
Simple Assignment (`=`)
The production `AssignmentExpression` :
`LeftHandSideExpression` = `AssignmentExpression`
is evaluated as follows:
1. Evaluate LeftHandSideExpression.
2. Evaluate AssignmentExpression.
3.Call GetValue(Result(2)).
4.Call PutValue(Result(1), Result(3)).
5.Return Result(3).
基本上,第4步put操作对(foo.bar = foo.bar) 没有影响,关键在于第5步返回了第3步得到的值,而第3步得到的值,其实它并没有一个base object。