js中的内存分为栈内存和堆内存,基本类型变量的变量名和值都是存储在栈内存中,而引用类型值的变量名和“值“也存储在内存,不同的是,这个“值”只是一个指针,指向的是存储在堆内存中,引用类型变量的真正数据段。

变量

基本类型值

指的是简单的数据段,基本的数据类型:UndefinedNullBooleanNumberString。这五种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。

引用类型值

指的是那些可能由多个值构成的对象,JavaScript中不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象,为此,引用类型的值是按引用访问的。

两种数据类型值的特点

动态的属性

创建一个变量并为该变量赋值。当这个值保存到变量中以后,对不同类型值可以执行的操作却大相径庭。

对引用类型的值
我们可以为其添加属性和方法,也可以改变和删除其属性和方法

1
2
3
var person = new Object();
preson.name = "Nicholas";
alert(person.name); //"Nicholas"

对基本类型值
我们不能给基本类型的值添加属性,尽管这样做不会导致任何错误

1
2
3
var name = "Nicholas";
name.age = 27;
alert(name.age) //undefined

在这两个例子中,我们发现只能给引用类型值动态地添加属性,以便将来使用;而基本类型值不行;

赋值变量值

除了保存的方式不同之外,在从一个变量向另一个变量赋值基本类型值或者引用类型值时,也存在不同。
PS.变量间的复制其实就就是栈内存的复制,由于基本类型值的值和类型都保存在栈内存中,所以基本类型值间的复制,ECMAScript在栈内存中新开辟了一个空间存储了另一个一模一样的基本类型值;而因为引用类型值存储在栈内存中的只是对象在堆内存里的地址,所以引用类型值之间的复制,只是ECMAScript在栈内存中新开辟的一个空间存储了一个一模一样的地址,而对象是保存在堆内存中的,也因此,这两个引用类型值引用的其实是同一个对象。

对基本类型值
如果从一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新值,然后把该值复制到新变量分配的位置上。

1
2
var num1 = 5;
var num2 = num1;

在此,num1中保存的值是5.当使用num1的值来初始化num2时,num2中也保存了值5。但是num2中的5和num1中的5是相互独立的,该值只是num1中的一个副本。此后,这两个变量可以参与任何操作而不会相互影响。

对引用类型值
当从一个变量向另一个变量复制引用类型的值时,其实是复制了这个变量在堆内存中的对象的指针。

1
2
3
4
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name) //"Nicholas"

复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响到另一个变量。

传递参数

在ECMAScript中,所有函数的参数都是按值传递的。也就是说,把函数外部的值赋值给函数内部的参数,就和把值从一个变量复制给另一个变量一样。基本类型值的传递如同基本类型变量的复制一样而引用类型值的传递则如同引用类型值的复制一样因为访问变量有按值和按引用两种方式,而参数只能按值传递。

在向参数传递基本类型的值的时候,被传递的值会被复制给一个局部变量(命名参数,或者arguments对象中的一个元素)。

1
2
3
4
5
6
7
8
9
function addTen(num){
num+=10;
return num;
}

var count = 20;
var result = addTen(count);
alert(count); //20,没有变化
alert(result); //30

而向参数传递引用类型的值时,就相当于把引用类型值复制给函数中的局部变量(把栈内存里的指针复制给函数变量),因此他们指向的对象在堆内存中都是同一个。

1
2
3
4
5
6
7
8
function setName(obj){
obj.name = "Nicholas";
}

var person = new Object();
setName(person);
alert(person.name); //"Nicholas",person新增了一个name属性,
//这是因为函数setName中的obj指向的对象跟全局对象person是同一个对象。

在局部作用域中修改的对象会在全局作用域中反映出来,这就说明参数是按引用传递的。可以看下面的例子,证明对象是按值传递的:

1
2
3
4
5
6
7
8
9
function setName(obj){
obj.name = [webpack 手记-1](media/15199091383517/webpack%20%E6%89%8B%E8%AE%B0-1.md)"Nicholas";
obj = new Object(); //这里将新增的局部变量赋值给obj
obj.name = "Greg"; //并修改了其name属性
}

var person = new Object();
setName(person);
alert(person.name); //"Nicholas"

这里最终person的属性name还是Nicholas。说明即使在函数内部修改了参数的值,但原始的引用仍然保持不变。实际上,当函数内部重写obj时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。
这里,大可把ECMAScript函数的参数想象成局部变量

引用类型的浅拷贝

众所周知,在引用类型之中,如果单纯地将一个引用类型的变量赋值给另一个变量,那么他们实际上指向的都是堆内存中的同一个对象。那么有没有方法可以实现引用类型的复制呢?答案是有的,就是浅拷贝和深拷贝。

浅拷贝
对引用类型中的基本类型进行拷贝,拷贝后的引用类型值跟母版是不同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xh={
age:18,
score:4
}
var xm=xh;//这种两个变量中存储的其实是同一个引用类型

function copyObj(obj){
var newObj={};
for(var key in obj){
newObj[key]=obj[key];
}
return newObj;
}
xm = copyObj(xh);//这样新变量中就存有母版引用类型的拷贝了

之所以叫做浅拷贝,就是因为这种方法不能进行深层次的拷贝,如果母版对象中含有其他引用类型的话,拷贝完之后,跟子版引用类型指向的其实是同一堆数据。由此,传说中的真·复制大法就是深拷贝

引用类型的深拷贝

待续

检测类型

typeof操作符是确定一个变量是字符串、数值、布尔值、还是undefined的最佳工具。如果变量的值是null或者对象,则typeof操作符会返回”object”。
根据规定,所有引用类型的值都是Object的实例。

当想进一步细分引用类型的值时,可以使用instanceof操作符,其语法如下所示:

result = variable instanceof constructor

1
2
3
alert(person instanceof Object);    //变量person是Object吗?
alert(person instanceof Array); //变量person是Array吗?
alert(person instanceof RegExp); //变量person是RegExp吗?

instanceof操作符检测基本类型值时始终会返回false,因为基本类型不是对象。
instanceof操作符检测一个引用类型值和Object构造函数时,会始终返回true。

两种类型的比较

基本类型 引用类型
不可修改 可以修改
保存在栈内存中 保存在堆内存中
按值访问 按引用访问
比较时,值相等则想得 比较时,同一引用才星等
复制时,创建一个副本 复制的其实是指针
按值传递参数 按值传递参数
typeof检测类型 instanceof检测类型

###执行环境和作用域

执行环境(execution context,为简单起见,有时也称为“环境”)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

关于执行环境,有几点很重要

  • 全局执行环境是最外围的一个执行环境
  • 在web浏览器,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。
  • 当某个执行环境中的所有执行代码执行完毕后,该环境被销毁,保存在其中的变量和函数定义也随之销毁。
  • 全局执行环境直到应用程序退出——例如关闭网页或者关闭浏览器——时才会被销毁



当代码在一个环境中执行时,会创建一个变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。

如果这个环境是函数,则将其活动对象(activation object)作为变量对象。

在作用域链的前端,始终都是当前执行的代码所在环境的变量对象的,活动对象最开始时只包含了一个变量,既arguments对象(这个对象在全局环境下是不存在的)。作用域链中的下一个变量对象来自包含环境(或称外部环境),而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。



标识解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域的前端开始,然后逐级地向后回溯,知道找到标识符为止(如果找不到标识符,通常会发生错误)。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var color = 'blue';     //这里是全局环境

function changeColor(){
var anotherColor = 'red';

function swapColor(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;

//这里可以访问color、anotherColor和tempColor
}
//这里可以访问color和anotherColor,但不能访问tempColor
}

//这里只能访问color
changeColor();

以上代码中涉及3个执行环境:全局环境、changeColor()的局部环境和swapColor()的局部环境。
全局环境中有一个变量color和一个函数changeColor()。
而changeColor()的局部环境中有一个名为anotherColor的变量和一个名为swapColor()的函数,但它可以访问全局环境中的变量color。
最后swapColor()的局部环境中有一个变量tempColor,该变量只能在这个环境中访问到。无论是全景环境还是changeCOlor()的局部环境都无权访问tempColor。然而,在swapColor()内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。
下图形象地展示了前面这个例子的作用域链。

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境都是线性的,有次序的。
函数参数也被当做变量来对待,因此其访问规则与执行环境中的其他变量相同。

*内部原理:每个函数都有自己的 执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。

没有块级作用域

在C、C++、Java等其他类C的语言中,由花括号封闭的代码都有自己的作用域(在ECMAScript中的说法就是它们自己的执行环境),当代码块执行完毕后被销毁;然而,在JavaScript中,if或者for语句中的变量声明会将变量添加到当前的执行环境中,尤其是在使用for语句时要牢记这一点

1
2
3
4
5
for(var i=0;i<10;i++){
doSomething(i);
}

console.log(i) //10;这里是在全局中打印“for语句中”的变量i

查询标识符

如前所述,每次加载一个新执行环境的时候,都会创建一个用于搜索变量和函数的作用域链。因此,当在某个环境中为了读取或写入而引用一个标识符是,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到了该标识符,搜索过程停止,变量就绪。如果在局部环境中没有找到该变量名,则继续沿作用域链向上搜索,一直追溯到全局环境的变量对象。**如果在全局环境中也没有找到这个标识符,则意味着该变量尚未声明。

1
2
3
4
5
6
7
var color = 'blue';

function getColor(){
return color;
}

console.log(getColor()); //"blue"

这里调用函数,首先搜索getColor()变量对象中是否包含一个color的标识符。在没有找到的情况下,向上到父环境(window),然后在那里找到了color。

1
2
3
4
5
6
7
8
var color = 'blue';

function getColor(){
var color = "red";
return color;
}

console.log(getColor()); //"red";

修改后的代码在getColor()函数中声明了以个color局部变量。调用函数时,该变量就会被声明。当第二段代码执行时,意味这必须找到并返回变量color的值。搜索过程中,首先会从局部环境中开始,而且在这里发现了一个名为color的变量,其值为“red”。因为变量已经找到了,所以搜索就停止。
*也就是说,任何位于局部变量color的声明之后的代码,如果不适用window.color都无法访问全局变量

js解析机制

预解析
在js引擎开始逐行解析代码之前,会先进行预解析

1.查找所有var 后面的变量,声明并赋值为undefined
2.查找所有function,声明并赋值
3.函数中的参数声明并赋值为undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//变量通过var声明
console.log(a); //undefined
var a=1;

//
var b=2;
function fn(){
console.log(b);
var b=1;
}
fn(); //undefined
console.log(b) //2

/*--------------------------*/

//变量不通过var声明的话
console.log(c);//Error;
c = 1;

//
var a = 1;
function fn(){
console.log(a);
a=2;
}
fn();//1
console.log(a);//2

/*--------------------------*/

//如果函数中有同名形参
var a=1;
function fn(a){
console.log(a);
a=2;
}
fn(); //undefined
console.log(a); //1

//
var a = 1;
function fn(a){
console.log(a);
a=2;
}
fn(a);//1
console.log(a);//1

如果在同一个script标签中预解析的时候碰到同名冲突,那么那么遵循函数>变量,后面的覆盖前面的声明的两个原则

1
2
3
4
5
6
7
8
9
10
11
12
13
//变量与函数冲突
var a=1;
function a(){}
console.log(a); //function

//函数与函数冲突
function b(){
console.log("first");
}
function b(){
console.log("second");
}
b(); //second

有一个很能说明上述关系的极端的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
console.log(a);
var a=1;
console.log(a);
function a(){
console.log(2);
}
console.log(a);
var a=3;
console.log(a);
function a(){
console.log(4);
}
console.log(a);
a();

//控制台上输出
/*
function a(){console.log(4);}
1
1
3
3
Error:a not a function
*/

参考链接