ECMAScript变量包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是简单的数据段,而引用类型值指的是那些可能由多个值构成的对象。在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。基本类型是按值访问的,引用类型是保存在内存中的对象。
与其他语言不同,js不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。
实际上,当复制保存着对象的某个变量时,操作的是对象的引用。但在为对象添加属性时,操作的是实际的对象。
var person = new Object()
person.name = "Nichjolas"
console.log(person.name)//"Nichjolas"
以上代码创建了一个对象并将其保存在了变量person中,然后,我们为该对象添加了一个名为name的属性,并将字符串值”Nichjolas”赋给了这会属性。紧接着,又访问了该属性。如果对象不被销毁或者这个属性不被删除,则这个属性会一直存在。
但是不能给基本类型添加属性:
var person = "Nichjolas"
person.age = 27
console.log(person.age)//undefined
复制变量值
对基本数据类型的复制:
var num1 = 5
var num2 = num1//使用num2来复制num1的值。复制的是基本类型Number
复制前的变量:
num1 | 5(Number类型) |
---|---|
如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。
复制后的变量:
num1 | 5(Number类型) |
---|---|
num2 | 5(Number类型):这里的5是num1的一个副本 |
当从一个变量向另一个变量复制引用类型的值,也会在变量对象上创建一个新值,然后把原来的引用类型的值复制一份给它,不同的是这个值是一个指针,而这个指针指向存贮在堆中的一个对象。复制操作结束后,两个指针实际上指向的是同一份对象。所以,改变其中一个就会引起变量的变化
var obj1 = new Object()
var obj2 = obj1 //复制一个指向obj1的堆中对象的指针给obj2
obj1.name = "pp"//改变obj1中的值。则obj2中的值也会发生改变
obj2.name//"pp"
复制前的变量对象:
obj1 | (object类型)指向一个堆中的对象 |
复制后的变量对象:
obj1 | (object类型)指向一个堆内存中的对象(一个指针) |
obj2 | (object类型)指向一个堆内存中的对象(一个指针):一个obj1的副本 |
传递参数
ECMAScript中所有的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。基本类型值的传递就如同今日类型变量的复制一样,而引用类型值的传递,则如同它自身的复制一样。
函数都是按值传递的
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript来说,就是arguments对象中的一个元素。)在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。
function addTen(num){
num += 10
return num
}
var count = 20
var result = addTen(count)
count//20
result//30
以上的代码表示函数内部有一个参数num,它实际是上是一个内部变量,当count传入进去的时候,num会得到count的一个副本(相当于count复制了自己一份给num),在外部来看count不会改变,num会根据函数中的代码进行改变。
function setName(obj){
obj.name = "pp"
}
var person = new Object()
setName(person)
person.name//"pp"
person和obj都会指向同一个堆内存中的对象。
function setName(obj){
obj.name = "pp"
obj = new Object()//指向新的堆内存变量对象
obj.name = "pr"//给新的堆内存变量对象赋上新的值"pr"
}
var person = new Object()
setName(person)
person.name//"pp"
person传入setName()的obj中,获得属性name,接着obj再声明了一个局部变量,并把该局部变量的name属性设置为新的值。在函数执行完毕后,新局部的值会被销毁。而person.name依旧是开始的那个’pp’。说明函数的传参在js中都是按值传递的。
检验类型:参考来自博客园的同学链接以及我们的红宝书
1. typeof
在处理基本类型的数据时, typeof
关键字就够用了
var s = "pp"
var b = true
var i = 22
var u
var n = null
var o = new Object()
typeof s //"string"
typeof b //"boolean"
typeof i //"number"
typeof u //"undefined"
typeof n //"object"
typeof o //"object"
2.instanceof
但是遇见引用类型的数据时, typeof
就没有什么用处了,因为我们是想要知道引用类型的具体类型,也就是说是
想知道它是什么类型的对象。
语法:
result = variable instanceof constructor
如果变量是给定引用类型(根据它的原型链来识别)的实例,那么 **instanceof **
操作符就会返回 true
。
person instanceof Object//person是 Object吗
colors instanceof Array //变量colors是Array吗
pattern instanceof RegExp //变量pattern是RegExp吗
function a (){}
typeof a
//"function"
a instanceof Object
//true
a instanceof Function
//true
typeof /\1/ //对正则使用typeof会返回object
//"object"
根据规定,所有引用类型的值都是Object的实例。因此,在检查一个引用类型值和object构造函数时,
instanceof
始终返回true. ECMAScript规定任何在内部实现[[call]]
方法的对象都应该在应用typeof
检测正则表达式时,由于规范的原因,这个操作符也返回"function"
3.constructor
原理:每一种数据类型在js引擎中都是以构造函数的形式存在的
比如:
Number
//ƒ Number() { [native code] }
console.log(("1").constructor === String)
//true
console.log((1).constructor === Number)
//true
console.log((true).constructor === Boolean)
//true
console.log(([]).constructor === Array)
//true
console.log(({}).constructor === Object)
//true
console.log((function(){}).constructor === Function)
//true
把上面的都看成是实例比如[]
就是Array构造函数的一个实例,这样就会是正确的
不过如果我们把构造函数的原型指向新的对象时,constructor也会改变,这种情况下,它就变得不可以使用了
function Fn (){}
Fn.prototype = new Array
var f = new Fn()
f.constructor === Fn//false
f.constructor === Array//true
//原因
f.__proto__ == Fn.prototype
f.__proto__.constructor ===f.constructor
f.__proto__.constructor===Fn.prototype.constructor===Array
4 终极大佬的方法:必记:它是来自jquery的源码,是其中的数据类型判定方法:Object.prototype.toString.call()
**toString()**
方法返回一个表示该对象的字符串。
Object.prototype.toString.call(null)
//"[object Null]"
Object.prototype.toString.call(undefined)
//"[object Undefined]"
mdn文档对toString()的描述: 可以通过
toString()
来获取每个对象的类型。为了每个对象都能通 过Object.prototype.toString()
来检测,需要以Function.prototype.call()
或者Function.prototype.apply()
的形式来调用,传递要检查的对象作为第一个参数,称为thisArg
。
Object.prototype.toString.call(new RegExp)
//"[object RegExp]"
Object.prototype.toString.call(new Date)
//"[object Date]"
改变原型后:
function Fn(){}
Fn.prototype = new Array()
var f = new Fn()
Object.prototype.toString.call(Fn)
//"[object Function]"
Object.prototype.toString.call(f)
//"[object Object]"
在这里再复习一个new操作符背后 js引擎的工作:
- 创建一个新对象
- 把 new 后构造函数的作用域给新的对象
- 执行构造函数中的代码
- 返回新的对象
执行环境和作用域
执行环境是js中最为重要的一个概念。执行环境定义了变量或函数有权访问其他数据,决定了它们各自的行为。每个执行环境都有一个与之相关的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。<br /> 全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表现执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是做为window对象的属性和方法创建的。某个执行环境中的所有代码执行毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)<br /> **每个函数都有自己的执行环境**。当执行流进入一个函数时,函数的环境就会被推入一个**环境栈**中。**而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境**。ECMAScript程序中的**执行流**正是这个方便的机制控制着。<br /> **当代码在一个环境中执行时,会创建变量对象的一个作用域链**。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。如果这个环境是函数,则将其_**活动对象**_**作为变量对象。**活动对象在最开始时只包含一个变量,即arguments对象(**这个对象在全局环境中是不存在的**)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域中的最后一个对象。<br /> 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)
var color = "blue"
function changeColor(){
if(color === "blue"){
color = "red"
}else{
color = "blue"
}
}
changeColor();
console.log("Color is now "+ color)
上面的代码中,changeColor()的作用域链它包含两个对象:它自己的变量对象(其中定义着arguments对象)和全局环境的变量对象。可以在函数内部访问变量color,就是因为可以在这个作用域中找到它。
此外,在局部作用域中定义的变量可以在局部环境中与全局变量互换使用,如下面这个例子所示:
var color = "blue"
function changeColor(){
var anotherColor = "red"
function swapColor(){
var tempColor = anotherColor
anotherColor = color
color = tempColor
//
}
swapColors()
}
changeColor();
上面的代码包含了三个执行环境:全局环境、 changeColor()
的局部环境和 swapColors()
的局部环境。全局环境中有一个变量 color
和一个函数 changeColor()
。 changeColor()
的局部环境中有一个名为 anotherColor
的变量和一个函数 swapColor()
,但是它也可以访问全局变量环境中的变量 color
, swapColor
中有一个变量 tempColor
,该变量只能在这个环境中访问到。
无论局部环境还是 changeColor()
的局部环境都无权访问 tempColor
。然而在 swapColor()
内部可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。
内部可以访问外部,但是外部是不可以访问内部的。这些环境之间的关系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
延长作用域链:
虽然执行环境的类型总共只有两种——全局和局部(函数),但是还有其他办法来延长作用域链。当执行流进入下列任何一个语句时,作用域链就会被加长:
- try-catch语句的catch块
- with语句
这两个语句都会在作用域链的前端加一个变量对象。对with语句来说,会将指定的对象添加到作用域链中。对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
function buildUrl(){
var qs = "?debug = true"
with(location){
var url = href + qs
}
return url
}
window.location//一个关于该网页的对象,里面有href等相关信息
出现with语句或try-catch语句,变量的作用域链可以从外面可以访问到里面
没有块级作用域
js没有块级作用域经常会导致理解上的困惑。在其他类的C的语言中,由花括号封闭的代码块都有自己的块级作用域(es5之前)。
if(true){
var color = "blue"
}
console.log(color)//
对于for语句来说:即使for循环完毕后,变量依旧在外部的执行环境中(es6的 let
, const
可以避免从外部取到,因为它实现了块级作用域)
for(var i = 0;i < 10 ;i++){
//dosomething
}
console.log(i)//10
对于下面的代码来说, sum
被 var
定义在函数内部,它在外面是取不到的,但是在上面的 for
语句中外面是可以取到里面的 i
的,这是他们一个是函数环境,一个是在全局环境定义的啦。
function add(num1,num2){
var sum = num1 + num2
return sum
}
var result = add(10,20)
console.log(sum)// Uncaught ReferenceError: sum is not defined
如果在函数内部没有对 sum
进行 var
的定义的话,那么默认这个 sum
变量为全局变量。如下所示
function add(num1,num2){
sum = num1 + num2
return sum
}
add(10,20)
console.log(sum)//30
查询标识符
在下面的例子中全局变量有 color="blue"
的 color
和函数 getColor()
,return 的color会首先在函数作用域中找,找到值后会停止搜寻,所以我们看到的就是red,而不是blue。
var color = "blue"
function getColor(){
var color = "red"
return color
}
getColor()//"red"
window.color//"blue"
垃圾收集
标记清除
js中最常用的垃圾收集方式是标记清除。当变量进入环境时,就将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。垃圾收集器在运行的时候会给存贮在内存中的所有变量都加上标记。最后垃圾清除器会完成内存清除工作。(2008年之前主流浏览器常用)
引用计数
含义:引用计数的含义是跟踪记录每个值被引用的次数。当申明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1.如果同一个值又被赋给另一个变量,则改值加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则减1。当这个值的引用次数变成0时,则说明没有办法再引用这个值了,因此就可以将其内存收回来。
但它有缺陷:
循环引用: 对象A包含对象B的指针,对象B包含对象A的指针。那么引用计数一直都是2,内存一直都清除不了。解除方法:将造成循环引用的变量都设为null。
管理内存:将不再使用的数据设置为null来接触引用,以便垃圾回收器下次运行时将其回收。
**