变量
- Primitive Values(基础变量):null,undefined, String,Number,Boolean
- Reference Values(引用变量):Object that made up of multiple values(就是一种由许多变量组成的对象)
- 基础变量:accessed by value,即直接操作是储存在变量中的值
- 引用变量:因为他stored in memory中,但是JavaScript又不允许直接操作内存,所以你操作object(对象)的时候,其实是在操作对它的引用(accessed by reference)。
- 注:在很多语言中,字符串是object,但JavaScript打破了这条传统。
Dynamic Property(动态属性)
- 只有引用型变量才能设置动态属性。
var person = new object();
person.name = "Carl";
alert(person.name);
Copying Values
//基础变量,对b进行任何操作不会对a产生影响
var a =1;
var b =a;
console.log(b);//1
//引用变量,y复制x,它们指向同一堆内存
var x =new Object();
var y =x;
y.height=8;
console.log(x.height);//8
JS是按值传递还是按引用传递
在分析这个问题之前,我们需了解什么是按值传递(call by value),什么是按引用传递(call by reference)。在计算机科学里,这个部分叫求值策略(Evaluation Strategy)。它决定变量之间、函数调用时实参和形参之间值是如何传递的。
按值传递(call by value)是最常用的求值策略:函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。
按引用传递(call by reference)时,函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值。
按引用传递会使函数调用的追踪更加困难,有时也会引起一些微妙的BUG。
按值传递由于每次都需要克隆副本,对一些复杂类型,性能较低。两种传值方式都有各自的问题。
我们先看一个C的例子来了解按值和引用传递的区别:
void Modify(int p, int * q)
{
p = 27; // 按值传递 - p是实参a的副本, 只有p被修改
*q = 27; // q是b的引用,q和b都被修改
}
int main()
{
int a = 1;
int b = 1;
Modify(a, &b); // a 按值传递, b 按引用传递,
// a 未变化, b 改变了
return(0);
}
这里我们可以看到:
a => p按值传递时,修改形参p的值并不影响实参a,p只是a的副本。
b => q是按引用传递,修改形参q的值时也影响到了实参b的值。
探究JS值的传递方式
JS的基本类型,是按值传递的。
var a = 1;
function foo(x) {
x = 2;
}
foo(a);
console.log(a); // 仍为1, 未受x = 2赋值所影响
再来看对象:
var obj = {x : 1};
function foo(o) {
o.x = 3;
}
foo(obj);
console.log(obj.x); // 3, 被修改了!
说明o和obj是同一个对象,o不是obj的副本。所以不是按值传递。 但这样是否说明JS的对象是按引用传递的呢?我们再看下面的例子:
var obj = {x : 1};
function foo(o) {
o = 100;
}
foo(obj);
console.log(obj.x); // 仍然是1, obj并未被修改为100.
如果是按引用传递,修改形参o的值,应该影响到实参才对。但这里修改o的值并未影响obj。 因此JS中的对象并不是按引用传递。那么究竟对象的值在JS中如何传递的呢?
按共享传递 call by sharing
准确的说,JS中的基本类型按值传递,对象类型按共享传递的(call by sharing,也叫按对象传递、按对象共享传递)。最早由Barbara Liskov. 在1974年的GLU语言中提出。该求值策略被用于Python、Java、Ruby、JS等多种语言。
该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。如下面例子中,不可以通过修改形参o的值,来修改obj的值。
var obj = {x : 1};
function foo(o) {
o = 100;
}
foo(obj);
console.log(obj.x); // 仍然是1, obj并未被修改为100.
然而,虽然引用是副本,引用的对象是相同的。它们共享相同的对象,所以修改形参对象的属性值,也会影响到实参的属性值。
var obj = {x : 1};
function foo(o) {
o.x = 3;
}
foo(obj);
console.log(obj.x); // 3, 被修改了!
对于对象类型,由于对象是可变(mutable)的,修改对象本身会影响到共享这个对象的引用和引用副本。而对于基本类型,由于它们都是不可变的(immutable),按共享传递与按值传递(call by value)没有任何区别,所以说JS基本类型既符合按值传递,也符合按共享传递。
var a = 1; // 1是number类型,不可变
var b = a;
b = 6;
据按共享传递的求值策略,a和b是两个不同的引用(b是a的引用副本),但引用相同的值。由于这里的基本类型数字1不可变,所以这里说按值传递、按共享传递没有任何区别。
基本类型的不可变(immutable)性质
基本类型是不可变的(immutable),只有对象是可变的(mutable). 例如数字值100, 布尔值true, false,修改这些值(例如把1变成3, 把true变成100)并没有什么意义。比较容易误解的,是JS中的string。有时我们会尝试“改变”字符串的内容,但在JS中,任何看似对string值的”修改”操作,实际都是创建新的string值。
var str = "abc";
str[0]; // "a"
str[0] = "d";
str; // 仍然是"abc";赋值是无效的。没有任何办法修改字符串的内容
而对象就不一样了,对象是可变的。
var obj = {x : 1};
obj.x = 100;
var o = obj;
o.x = 1;
obj.x; // 1, 被修改
o = true;
obj.x; // 1, 不会因o = true改变
这里定义变量obj,值是object,然后设置obj.x属性的值为100。而后定义另一个变量o,值仍然是这个object对象,此时obj和o两个变量的值指向同一个对象(共享同一个对象的引用)。所以修改对象的内容,对obj和o都有影响。但对象并非按引用传递,通过o = true修改了o的值,不会影响obj。
所以可以说JS既不是按值传递也不是按引用传递,而是按共享传递。
Determing Type
利用typeof:
var a = "123";
var b = 123;
var c = true;
var d;
var e = null;
var f = new Object;
console.log(typeof(a));//string
console.log(typeof(b));//number
console.log(typeof(c));//boolean
console.log(typeof(d));//undefined
console.log(typeof(e));//object
console.log(typeof(f));//object
如果某一个变量是某一类型实例的话,执行instanceof操作符就会返回true
alert(person instanceof Object) //对象
alert(array instanceof Array) //数组
alert(pattern instanceof RegExp) //正则
- 所有引用变量执行instanceof都会返回true,
- 所有基础变量执行instanceof都会返回false。
执行上下文和作用域
每一个可执行上下文都有一个相关联的variable object,用来存放它定义的变量和函数。这个object无法通过代码获取,但是在各个场景后面处理数据。
全局执行上下文(global execution context)是最重要的一个。在网络浏览器中,全局上下文是 window object,全局的变量和函数都相当于是window这个对象的属性和方法。网页或浏览器关闭时,全局执行上下文结束。
- 每一个函数都有它自己的执行上下文(execution context)。当执行到某一个函数时,此函数的execution context被压入context栈。当此函数执行结束时,函数的context被栈弹出,将控制权返还给压栈前的执行上下文环境。这种control execution flow贯穿整个JavaScript程序。
扩大作用域链
- 基本作用域有两种,global和function(第三种在调用eval()时)
* 扩大作用域链方式:with和try catch中的catch区块(block),它们都在作用域链的前段添加了一个variable object
没有块(block)级别作用域
for(var i=0;i<10;i++){
console.log(i);
}
console.log(i);//10
//“函数级”作用域
function adds(A,B){
var i = A+B;
return i;
}
var result = adds(3,10);//13
console.log(i);//error
标识符(JavaScript Identifier)查询
查询从scope chain的起始开始,如果从local context查到,停止查询,如果没查到会按照scope chain继续查询(这里包含各种原型链(prototype chain)),这个过程一直持续到global context,如果没有查到,就是这个Identfier没有被声明。
var color = "red";
function getColor(){
return color;
}
console.log(getColor());
垃圾回收
- JavaScript内存自动管理
Mark and Sweep
当变量出现(comes into)在上下文,比如在函数中声明一个变量,它就会因为在这个上下文(context)里而被标记;逻辑上,在函数执行的过程中变量的内存是不会被释放的;当上下文结束(goes out of context)时,变量同样会因为上下文结束而被标记。
当garbage collector启动时,它标记了所有储存在内存中的变量,然后它会删除那些在context中的变量以及它们所引用的那些变量的标记,然后标记那些要被删除的变量,因为它们无法通过context中的变量获取了。最后garbage collector就删除这些标记了的变量,从而把它们所占用的内存重新释放。
Reference Counting
a serious issue: circular references
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;//reference count 2
objectB.anotherObject = objectA;//reference count 2, can't release memory.
}
- Mark and Sweep is better now.
Managing Memory
如果某一数据不再使用,最好把它设置为null,释放引用(dereferencing a value)。这条建议通常针对全局对象和它的属性,局部变量不用担心,当context结束,局部变量会自动被释放。
function A(name){
var k =new Object();
k.name=name
return k;
}
var globalOne = A("test");//A函数执行完,k自动释放
globalOne=null;//手动释放
- dereferencing a value并不是自动回收它所关联的内存,关键是确保变量已经离开context,从而在下次garbage collector 启动时,内存被回收。
总结
JavaScript变量有两种类型:基础变量 引用变量
- 基础变量包括:null undefined number string boolean
- 基础变量大小固定,存储在栈上
- 拷贝基础变量就是创建了这个变量的备份
- 引用变量是对象,存储在堆(heap)上
- 一个包含引用值的变量实际上只是包含了指向这个对象(object)的指针,而不是对象本身。
- 拷贝另一个对象的引用只是拷贝了指针,所以两个引用都指向同一个对象。
- typeof 判断基础变量的类型,instanceof 判断引用变量类型
所有变量,不管基础的,引用的都存在于执行上下文,执行上下文决定了变量的生命周期,并且部分代码能够访问它
- 可执行上下文存在全局的和函数中的两种
- 每次进入执行上下文时,它都会创建一个用于搜寻变量和函数的作用域链。
- 函数的本地上下文可以访问本地变量,包含它的上下文和全局上下文。
- 全局上下文只能访问全局的函数和变量,不能访问局部的上下文中的任何数据。
- 可执行上下文中的变量有助于决定什么时候释放内存。
JavaScript是一个垃圾回收编程环境,开发者无需关心内存分配和回收。
- 变量离开作用域(go out of scope)会自动被标记为待回收的变量,在下次垃圾回收器执行的时候被删除。
- 占主导地位的垃圾回收算法是标记删除机制
- 另一种是计数器算法,除了i.e.浏览器以外很少用了。问题出在循环引用。
- 而标记删除算法能完美解决这种问题。