基本类型和引用类型的值
(1)概念
基本类型的值指的是简单的数据段,而引用类型的值指那些可能由多个值构成的对象。
引用类型的值是保存在内存中的对象。JavaScript 不允许直接操作对象的内存空间,实际上操作的是对象的引用,而不是实际的对象。引用类型的值是按引用访问的。
两种类型的值得在内存中的位置是不一样的,基本类型值在内存中占据固定大小的空间,被保存在栈内存中;引用类型的值时对象,保存在堆内存中。
包含引用类型值得变量实际上包含的并不是对象本身,而是一个指向该对象的指针,也可以理解为该对象在堆内存中的地址,而这个变量是保存在栈内存中的。
因此,复制引用类型的值时,实际上复制的是指向储存在堆中的对象的指针。它们会指向同一个对象,改变其中一个变量会影响到另一个变量。
(2)函数传递参数
ECMAScript 中所有函数的参数都是按值传递的。
关于按值和按引用传递:
- 按值传递(call by value):函数的形参是被调用时所传实参的副本,修改形参的值并不会影响实参,是最常用的求值策略。
- 按引用传递(call by reference)时,函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值。
- 按值传递由于每次都需要克隆副本,对一些复杂类型,性能较低;按引用传递会使函数调用的追踪更加困难,有时也会引起一些微妙的BUG。
JS中,函数参数按值传递有什么特点?
- 传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或
arguments
对象的一个元素); - 传递引用类型的值时,会把这个值在内存中的地址(指针)复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。
这部分例子可看大神博客,其中提到按共享传递 call by sharing的思想
例1:向函数参数传递引用类型值
var foo = {name:'foo'};
function test(obj){
obj.name = 'test'; //第一步操作
obj = {name:'bar'}; //第二步操作
};
test(foo);
console.log(foo); //结果:Object {name: "test"}
首先,这里
foo
保存的是对象的指针,不是对象本身,按值传递给参数obj
的也就是这个指针,称为对象1;
第一步操作,obj
指向的对象仍为对象1,此时修改了对象1的name
属性;
第二步操作,给obj
赋了新值,指向了新的对象,称为对象2;相当于执行了obj = new Object(); obj = {name:'bar'};
这里obj
获得一个全新的对象。
在全局环境下,变量foo
没有重新赋值,一直是指向对象1;
所以改变函数内部变量obj
的值,不会影响外部环境中参数原值foo
。
(3)检测类型
- 使用
instanceof
操作符判断一个实例是否属于某种类型; - instanceof 可以在继承关系中用来判断一个实例是否属于它的父类型,在多层继承关系中,
instanceof
运算符同样适用; - 所有引用类型的值都是
Object
的实例,检测一个引用类型值和Object
构造函数时,instanceof
操作符始终返回true
。
要深入理解 instanceof
操作符,会涉及到原型继承机制,后续章节中会提及。此博客中有一些例子。
执行环境和作用域
(1)概念理清
- 执行环境:定义了变量或函数有权访问的其他数据,决定了它们各自的行为,有时也被叫做执行上下文。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数也随之销毁。执行环境有全局执行环境和函数执行环境之分。
- 变量对象:环境中定义的所有变量和函数都保存在这个对象中。
- 宿主环境:宿主环境一般由外壳程序创建和维护,可以为JavaScript等多种脚本语言提供服务。宿主环境一般会创建一套公共对象系统,这套对象系统对所有脚本语言开放,并允许它们自由访问。同时,宿主环境还会提供公共接口,用来装载不同的脚本语言引擎。这样我们可以在同一个宿主环境中装载不同的脚本引擎,并允许它们共享宿主对象。
- 全局执行环境:全局执行环境是最外围的一个执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。代码载入浏览器时,全局执行环境被创建;当我们关闭网页或者浏览器时全局执行环境才被销毁。
- 作用域链:作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。
- 活动对象:此概念针对函数。在一个函数对象被调用的时候,会创建一个活动对象,首先将该函数的每个形参和实参,都添加为该活动对象的属性和值;将该函数体内显示声明的变量和函数,也添加为该活动的的属性。这个活动对象作为该函数执行环境的作用域链的最前端,并将这个函数对象的[[scope]]属性里作用域链接入到该函数执行环境作用域链的后端。
- 函数对象:在一个函数定义的时候, 会创建一个这个函数对象的
[[scope]]
属性,并将这个[[scope]]
属性指向定义它的作用域链上。
(2)延长作用域链
- 在作用域链前端临时添加一个变量对象,该变量对象会在代码执行后被移除。
try-catch
语句的catch
模块:当try代码块中发生错误时,执行过程会跳转到catch
语句,然后异常对象推入一个可变对象并置于作用域的头部;在catch
代码块内部,函数的所有局部变量将会被放在第二个作用域链对象中。with
语句:with
语句是对象的快捷应用方式,用来避免书写重复代码。width
语句看上去更高效,实际上产生了性能问题,一般不建议使用。
(3)块级作用域
块级作用域和函数作用域
块级作用域:变量在离开定义的块级代码后立即被回收;
函数作用域:变量在定义的环境中以及嵌套的子函数中处处可见;
ES5 及以前版本 JavaScript
都没有块级作用域,只有两种:函数作用域和全局作用域。使用 var 声明的变量会自动被添加到最接近的环境中。
直到 ES6 中定义了块级作用域,使用 let
/ const
声明的变量只能在块级作用域里访问。
let
声明的特点:
let
所声明的变量,只在let命令所在的代码块内有效;- 不存在变量提升,变量一定要在声明后使用,否则报错;
- 只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。在代码块内,使用
let
命令声明变量之前,该变量都是不可用的,称为“暂时性死区”(temporal dead zone,简称 TDZ); - 由于存在“暂时性死区”,在let命令声明变量之前,
typeof
操作符检查此变量也会报错; let
不允许在相同作用域内,重复声明同一个变量。
ES6 语法的学习移步阮一峰老师的《ECMAScript 6 入门》教程。
以上,就是《Javascript高级程序设计》第四章的学习笔记。