1.原始值与引用值
EcmaScript变量可以包含两种不同类型的数据:原始值、引用值
原始值:最简单的数据
引用值:由多个值构成的对象
六种原始值:
- Undefined
- Null
- Boolean
- Number
- String
- Symbol
把一个值赋值给一个变量的时候,JS引擎必须确定这个值是原始值还是引用值。
保存原始值的变量是按值访问的 ,因为我们操作的就是存储在变量种的实际值。
引用值是保存在内存中的对象,JS不允许直接访问内存的位置(即不能直接操作对象所在的内存空间)。在操作对象时,实际上操作的是对该对象的引用,而非实际的对象本身。为此,保存引用值的变量是按引用访问的。
1.1动态属性
原始值和引用值的定义方式 类似 ,创建一个变量,然后赋给它一个值。
不同之处是,变量保存这个值后,我们能对这个值做什么?
引用值:可以随时删除、添加、修改其属性、方法
let person = new Object();//创建一个新的对象
person.name = 'Macxx';//动态为这个新对象添加一个name属性
console.log(person.name);
person.name = 'New Macxx';//动态修改对象中的属性的值
console.log(person.name);
原始值:不能有属性,尽管给原始值添加属性不会报错
let name = 'XiaoMing';//原始值
name.age = 27;//给原始值添加属性name
console.log(name);
console.log(name.age);
原始类型的初始化只能使用原始字面量形式,如果使用new关键字,则JS会创建个Object类型的实例,但是其行为类似原始值。(什么叫其行为类似原始值?代码中似乎并没有表现出来。)
let name1 = 'Macxx';//使用原始字面量初始化
let name2 = new String('Diing');//使用new关键字初始化
name1.age = 27; //原始值不能添加属性
name2.age = 23; //对象可以
console.log(name1.age, typeof name1);
console.log(name2.age, typeof name2);
1.2 复制值
在通过变量,把一个原始值复制到另一个变量的时候,原始值会被复制到新变量的位置,这两个变量可以独立使用,互不干扰。
let num1 = 5;
let num2 = num1;//复制值
//两个变量独立使用,互不干扰
num1 += 1;
num2 += 10;
console.log(num1,num2);
把引用值从一个变量复制到另一个变量的时候,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是个指针,它指向存储在堆内存种的对象。 复制操作完成后,两个变量实际上指向同一个对象,因此,一个对象上面的变化会在另一个对象上面反映出来。
let name1 = {
age:27,
hair:"black"
}
let name2 = name1; //复制引用值
name2.age = 18;
name2.hair = "red";
console.log(name1);
变量比作小纸条,小明家就是一个存储在堆内存中的对象,我们假设有纸条A,上面写着小明家的地址,然后又拿来一个纸条B,把A上面的地址抄了下来,那么我们就可以通过纸条A去到小明家里,也可以通过纸条B去到小明家里。同时,我们通过纸条A去到小明家里的时候,在小明家里画了幅画(动态添加属性),那么当我们通过纸条B去到小明家里的时候,也能看到这幅画。
1.3 传递参数
在EcmaScript中,所有函数的参数都是按值传递的(即函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样)。
按值传递参数的时候,值会被复制到一个局部变量(即一个命名参数,或者说是arguments对象中的一个槽位)。
按引用传递参数的时候,值在内存中的位置会被保存在一个局部变量,这意味着对本地变量的修改会反映到函数的外部(这在EcmaScript中是不可能的)。
+++++++++手打分割线++++++++++++++++++++++++
若参数是个原始值,则按值传递很容易看出来:
addTen = (num) => {
return num += 10;
}
let count = 10 ;
let result = addTen(count);
console.log(count);//执行完函数addTen后,count的值并没有受到影响
console.log(result);
若参数是个对象,则开始让人思索了:
setName = (obj) => {
obj.name = 'Macxx';
}
let person = new Object();//定义一个Object对象
console.log('执行setName函数前',person.name);
setName(person);
console.log('执行setName函数后',person.name);
上面的代码,从结果来看,很像按引用传参,因为对本地变量的修改(似乎是)反映到了函数的外部,其实并不是,可以回顾一下上面【1.2 复制值】中的引用值复制,就能明白其中的缘由了。当然,我们还可以用下面的代码来证明对象也是按值传参的:
setName = (obj) => {
obj.name = 'Macxx';
obj = new Object();
obj.name = 'Diing';
}
let person = new Object();//定义一个Object对象
console.log('执行setName函数前',person.name);
setName(person);
console.log('执行setName函数后',person.name);
就上面的代码而言,可见在setName函数中加了两行代码。这两行代码的作用是改变了参数的地址值,如果对象是按引用传参的,那么执行完setName函数后person.name的值应该为Diing,结果很明显并不是,这是因为:
一开始小纸条A上面写着小明家的地址,然后我们把纸条A上的地址抄到了纸条B上面(按值传参),然后拿着纸条B去到了小明家里,打烂了一扇窗(执行obj.name = ‘Macxx’),然后我们把纸条B上面小明家的地址擦掉,写上了小红家的地址(执行obj = new Objector()),然后又根据纸条上的地址去到了小红家打烂了一个杯子(执行obj.name = ‘Diing’)。
纵观整个过程,纸条A上面的值没有改变,变的只有纸条B,但是我们通过纸条B去到小明家里打烂的那扇窗,当我们通过纸条A去到小明家里也是能看到的,不过却看不到被打烂的杯子,因为被打烂的杯子是小红家的,不是小明家的。
如果是按引用传递参数,那么就不应该是把纸条A的地址抄到纸条B上,而是整个过程都只有纸条A。
1.4 确定类型
1.typeof操作符
判断一个变量是否为字符串、数值、布尔值、或者undefined的最好方法就是用typeof操作符。但是,如果判断的值是对象或者null,那么typeof操作符就会返回一个’object’。
let num = 123;//数值
let str = 'Hello';//字符串
let variable;//undefined
let isTrue = true;//布尔值
let obj = {};//对象
let isNull = null;//null
console.log(typeof num);
console.log(typeof str);
console.log(typeof variable);
console.log(typeof isTrue);
console.log(typeof obj);
console.log(typeof isNull);
typeof对原始值很有用,但是对引用值却用处不大,因为很多时候我们并不关心一个值是不是对象,我们关心的是这个值是什么类型的对象。这个时候,我们就要用到instanceof操作符了。
2.instanceof操作符
语法: result = variable instanceof constructor
let arA = [1,2,3,4];
let objB = {
name:'Macxx',
age:26
}
let strC = 'Hello';
console.log( arA instanceof Array );//arA是Array类型的吗?
console.log( objB instanceof Object);//objB是Object类型的吗?
console.log( strC instanceof Object);//strC是Object类型的吗?
按照定义,所有的引用值都是object的实例,因此通过instanceof操作符检测任何object构造函数都会返回true,若用instanceof去检测原始值,则会始终返回false,因为原始值不是对象。
2.执行上下文与作用域
2.1执行上下文
变量或函数的上下文决定了它们能访问哪些数据以及它们的行为
每个上下文都有一个关联的 “变量对象”,而在这个上下文中定义的所有变量和函数都存于这个对象上。(我们无法通过代码去访问这个变量对象,但是后台处理数据的时候会用到它)
全局上下文 是最外层的上下文。在浏览器中,全局上下文就是我们的window对象。所有通过var定义的变量及函数都会成为window对象的属性和方法。
console.log(window);//打印出window对象
var name = 'Macxx';//在全局上下文用var声明变量
console.log(window.name);
使用let和const的顶级声明不会定义在全局上下文中,但是在作用域链解析效果上是一样的。
let hair = 'Black';
const age = 27;
console.log(window.hair,window.age);
上下文在其所有代码执行完毕后会被销毁,包括定义在它上面的所有变量和函数。(全局上下文在应用程序退出前才会销毁,比如说关闭网页、退出浏览器)
//定义一个函数,函数里面是函数上下文
function setName(){
let fun_name = 'setName';//在函数上下文中定义一个变量fun_name
console.log(fun_name);
}
setName();//执行函数
console.log('函数执行后',fun_name);
上面代码中的变量fun_name,在函数上下文中存在,函数执行完毕后,函数上下文被销毁,fun_name变量也被销毁了,所以setName函数执行完后再打印输出fun_name会报错。
每个函数调用都有自己的上下文,当代码执行流/进入到函数时,函数上下文被推到一个 函数上下文栈 ,在函数执行完后,上下文栈弹出该函数的上下文,将控制权返回给之前的上下文。
2.2 作用域链
ECMAScript程序的执行流就是通过上面说到的上下文栈控制的,上下文中的代码在执行的时候,会创建一个变量对象的作用域链。
这个作用域链决定了各级上下文中的代码在 访问变量和函数时的顺序
代码正在执行的上下文变量对象始终位于作用域链的最前端。
全局上下文的变量对象始终是作用域链的最后一个变量对象。
如果上下文是函数,则其“活动对象”用作变量对象。活动对象最初只有一个定义变量:arguments(全局上下文中没有这个变量)
//定义一个函数
function fun(e){
console.log(arguments);//函数的活动对象
}
fun();
console.log(window.arguments);//全局上下文没有arguments
作用域链中的下一个变量对象来自 包含上下文,再下一个变量对象来自下一个包含上下文。
上面的代码不难看出,fun上下文包含着setName上下文,setName上下文包含着color上下文。
当上面的代码中的函数正常执行的时候,作用域链的最前端是color的变量对象,color变量对象的后面是setName的变量对象,然后再后面是fun的变量对象,最后是window对象。
代码执行的时候,标识符解析是通过沿作用域链逐级搜索标识符名称完成的,搜索过程中始终从作用域链的最前端开始,逐级往后,知道找到标识符(找不到就报错)
var color = 'blue';
function changeColor(){
color = color === 'blue' ? 'red' :'blue';
}
changeColor();
console.log(color);
changeColor函数上下文中是没有定义color的,color是在全局上下文中定义的,但是执行changeColor函数没有报错,并且最后color的值也被改变了,这是因为,代码执行的时候,JS引擎通过作用域链,先搜索changeColor的变量对象,没找到color,然后往后一级,来到了window对象,搜索到了color,确定了color的值,进而执行判断,最后把color的值改为了red。
局部作用域链中定义的变量可用于在局部上下文中替换全局变量。
var color = 'blue';//定义全局变量
function ChangeColr(){
let anotherColor = 'yellow';//定义局部变量
function swapColor(){
//局部变量替换全局变量
[color,anotherColor]=[anotherColor,color];
}
swapColor();
}
ChangeColr();
console.log(color);
内部上下文可以通过作用域链访问外部上下文的一切,但是外部上下文却无法访问内部上下文的任何东西。
上下文之间的连接是 线性的、有序的
每个上下文都可以到上一级上下文中去搜索变量和函数,但是任何上下文都不能到下一级上下文中去搜索。
函数的参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的规则。
2.3 作用域链增强
某些语句会导致作用域链前段‘临时’添加一个上下文,这个上下文在代码执行后被删除。通常在两种情况下会出现这种情况:(即代码执行到下面的任意一种情况的时候)
- try/catch语句块的catch块
- with语句
这两种情况都会在作用域链前端添加一个变量对象:
- with语句会在作用域链前端添加一个指定的对象。
- catch语句则会创建一个新的变量对象,这个变量对象会包含所要抛出的错误对象的声明。
2.4 变量声明
1.使用var的函数作用域声明
在使用var声明变量时,变量会被自动添加到最接近的上下文。
在函数中,最接近的上下文就是函数的局部上下文,在with语句中,最接近的上下文也是函数的局部上下文。
如果变量未经声明,直接初始化,那么它就会自动被添加到全局上下文。
function fun(){
//函数的局部上下文
var color = 'blue';//使用var声明变量color,并初始化
}
fun();
console.log(color);//在函数外部打印color,报错
function fun(){
//函数的局部上下文
color = 'blue';//未使用var去声明color变量,而是直接初始化
}
fun();
console.log(color);//可以打印出color,因为color被添加到了全局上下文
2.变量提升
var声明会被拿到函数或者全局作用域的顶部,位于作用域中所有代码之前,这个现象叫做“提升”。提升让同一作用域的代码不必考虑是否已经声明就可以直接使用。(通过在声明前打印变量,可以验证提升)
function fun(){
console.log(name);//变量声明前打印出变量
var name = 'Macxx';//变量声明,并且初始化
console.log(name);//声明后打印出变量
}
fun();
可以看出,使用var声明的变量,存在变量提升,可以在声明前就使用它,不过此时该变量的值是undefined。
再详细一点理解变量提升,看下面的代码:
上面左边的代码name和age变量是在for循环后才进行声明的,但是因为使用的是var声明,存在变量提升,所以实际上是可以看成右边的代码,进入函数的时候,先把所有需要声明的变量和函数提到作用域的顶部进行声明,赋值undefined,然后再执行后面的代码。
3.使用let的块级作用域声明
块级作用域是由最近的一块包含花括号{}界定的。即if块、while块、function块等,甚至连单独的块也是let声明变量的作用域。
if(true){
var name = 'Macxx';
let age = 27;
}
console.log(name);
console.log(age);
上面的代码就可以看出什么叫块级作用域了,就是只能在这个块里面起作用,比如用let关键字声明的age变量,在if块外面就无法访问,而同样在if块里,但是是用var声明的变量name却能在外部访问。
let与var的另一个不同之处在于: 同一作用域内,不能声明两次。 重复的var声明会被忽略,但是重复的let声明则会抛出SyntaxError。
var name = 'Macxx';
var name = 'Diing';
let color = 'red';
let color = 'black';
可以看到,前面用var重复声明的name变量没有报错,但是到了用let重复声明color变量的时候,就报错了。
let的这种行为 非常适合在循环中声明迭代变量, 使用var声明迭代变量会泄漏到循环外部。
for(var i = 0 ; i < 5 ; i++){}
console.log(i);//迭代变量i被泄漏到了循环外部
for(let j = 0 ; j < 5 ; j++){}
console.log(j);//迭代变量j在循环结束后就被销毁了
严格来说,let在JS运行中也会提升,但是由于“暂时性死区”缘故,实际上不能在声明前使用let变量。
4.使用const的常量声明
使用const声明的变量必须在声明的同时初始化为某个值,一经声明,在其生命周期的任何时候都不能再重新赋予新的值。(其余规则和let一样)
const NAME = 'Macxx';//const声明常量NAME
NAME = 'Diing';//赋予新值,报错
const声明只应用到顶级原语或者对象。换句话说, 赋值为对象的const变量不能再被重新赋值为其他的引用值,但是对象的键则不受限制。
const person = {
name:'Macxx',
age:27,
hairColor:'black'
}
//改变person的键
person.hairColor = 'red'; //修改person的属性的值
person.skin = 'yellow'; //动态添加一个skin属性
console.log(person.hairColor, person.skin);
//赋值其他的引用值
person = {
color:'green'
}
上面的代码可以看出,用const声明的常量如果初始化为一个对象,那么我们修改这个对象的键(增删改)都是可以的,但是如果我们给这个常量赋予新的引用值,就会报错。
若想让整个对象都不能修改,可以用object.freeze(),这样再给属性赋值的时候,虽然不会报错,但是会静默失败。
const Obj = Object.freeze({});
Obj.name = 'Macxx';
console.log(Obj.name);
由于const声明暗示变量的值是单一类型且不可修改,JS运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。(这是一种优化的方法)
开发实践表明,在不影响开发任务的情况下,多使用const声明,可以从根本上保证提前发现重新赋值导致的Bug。
2.5 标识符查找
当在特定的上下文中为读取或者写入而引入一个标识符时,必须通过搜索而确定这个标识符表示什么。
搜索开始于作用域链前端,以给定的名称,搜索对应的标识符,一直搜索到全局上下文的变量对象。若找到了标识符,则停止搜索,变量确定,否则报错。
let thisColor = 'black';
function fun(){
//这里是fun的上下文
function setName(){
//这里面是setName的上下文
function color(){
//这里是color的上下文
let a = thisColor;
let b = thisname;
}
color();
}
setName();
}
fun();
浏览器报错显示thisname变量未定义,就是因为一直搜索,搜索到全局上下文的变量对象也没有发现有thisname这个变量,所以报错了。
标识符查找并非没有代价的,访问局部变量要比访问全局变量快,因为不用切换作用域。