JS基本类型和引用类型的值

ECMAScript是一种脚本语言规范。
JavaScript 是一种基于 ECMAScript 规范的脚本语言。

基本类型和引用类型的值

ECMAScript变量包含两种不同的数据类型,分别是:

  1. 基本类型值:简单的数据段。(五种基本数据类型:Undefined,Null, String,Number,Boolean)
  2. 引用类型值:可能由多个值构成的对象。
    引用类型的值是保存在对象中的,js不允许直接操作对象的内存空间,在操作对象时,是在操作对象的引用而不是实际的对象。

动态的属性

我们可以给对象添加属性和属性值,但是不能给基本类型的值添加属性。

1
2
3
4
5
6
var person = new Object()
person.name = 'leni'
console.log(person.name) // leni
var name = 'Nikku'
name.age = 18
console.log(name.age) // undefined

复制变量值

复制基本类型的变量值,两个变量是相互独立而互不影响的

1
2
var num1 = 5
var num2 = num1

复制引用类型的值,复制得到的值其实是一个指针,而这个指针指向存储在堆中的一个对象,两个变量实际上引用同一个对象。若改变其中一个变量,另外一个变量也会变。

1
2
3
4
var obj1 = new Object()
var obj2 = obj1
obj1.name = 'leni'
console.log(obj2.name) // leni

传递参数

ECMAScript中所有函数的参数都是按值传递的。就如同上面把值从一个变量复制给另一个变量一样。需要注意的是,访问变量是有按值和按引用两种方式,而参数只能按值传递。什么意思呢?

首先拿基本类型值举例,这个比较容易理解

1
2
3
4
5
6
7
function addTen(num){
    num+=10
    return num
}
var count = 10
console.log(addTen(count)) // 20
console.log(count) // 10 没有变化

就由复制变量那说的,两个变量(这里指count,num)是相互独立而互不影响的

现在拿引用类型举例,比较容易理解错误

1
2
3
4
5
6
function setName(obj){
    obj.name = 'leni'
}
var person = new Object()
setName(person)
console.log(person.name) // leni

person和obj都指向同一个对象。
再稍作修改

1
2
3
4
5
6
7
8
function setName(obj){
    obj.name = 'leni'
    obj = new Object()
    obj.name = 'Nikky'
}
var person = new Object()
setName(person)
console.log(person.name) // leni

person的name属性值仍为leni,有人可能会问为什么不是Nikky呢?
一开始person和obj都指向同一个对象,再给对象的name赋值之后,变量obj被赋值成了另一个对象,此时obj和person指向的不是同一个对象,所以后面obj.name = ‘Nikky’是新对象的name为Nikky,而person指向的对象的name并没有变。
实际上,当在函数内重写obj时,这个变量引用的是局部变量,它会在函数执行完毕后立即被销毁。

检测类型

typeof和instanceof的区别
typeof是返回类型,确定一个值是哪种基本类型;而instanceof是返回true或者false,确定一个值是哪种引用类型

typeof操作符是确定一个变量是Undefined,String,Number或者Boolean的最佳工具,如果变量的值是一个对象或者null,typeof会返回object

1
2
3
4
var u;
var n = null;
console.log(typeof u) // undefined
console.log(typeof n) // object

如果变量是给定引用类型(根据原型链识别)的实例,那么instanceof就会返回true

1
2
console.log(person instanceof Object) // true
// person是Object的实例

执行环境及作用域

执行环境(环境): 定义了变量或函数有权访问的其他数据,决定了它们的行为。它有助于垃圾回收机制确定何时释放内存。每个执行环境都有与之对应的变量对象。环境中定义的所有变量和函数都保存在这个对象中。两种类型:全局和局部

某个执行环境中的所有代码执行完毕之后,该环境会被销毁,其中的变量和函数定义也会被销毁。

每个函数都有自己的执行环境,当执行流进入函数之后,环境就会被推入到一个环境栈中;函数执行完毕之后,栈会将其环境弹出,把控制权返回之前的执行环境。

作用域链:当代码在一个环境中执行时,会创建变量对象的一个作用域链。它保证了对执行环境有权访问的所有变量和函数的有序访问。说白了,作用域链就是用来搜索变量和函数的。
作用域前端始终是当前执行的代码所在环境的变量对象,下一个变量对象来自包含环境(即外部环境),再下一个就是下一个的包含环境,一直到全局执行环境。全局执行环境的变量对象始终都是作用域链中的最后一个对象

1
2
3
4
5
6
7
8
9
10
11
12
var color = 'blue'
function changeColor(){
    var anotherColor = 'red'
    function swapColor(){
        // 可以访问color,anotherColor, tempColor
        var tempColor = anotherColor
        anotherColor = color
        color = tempColor
    }
    swapColor() // 可以访问color,anotherColor
}
changeColor() // 只能访问color

上面代码有3个执行环境:全局环境,changeColor()的局部环境,swapColor()的局部环境。环境可以访问父执行环境的变量和函数。

也就是说,内部环境可以通过作用域链访问所有的外部环境,但是外部环境并不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性的、有次序的。环境可以向上搜索作用域链,去查询变量和函数;但是不能向下搜索而进入另一个执行环境。

延长作用域链

有没有什么情况让变量对象能访问到所包含的变量对象的属性或函数,或者实现同级访问呢?
有两种情况可以有:

  1. try-catch语句的catch块
    对catch语句来说,它会创建一个新的变量对象,其中包含了抛出的错误对象的声明。
  2. with语句
    对with语句来说,它会将指定的对象添加到作用域链中。如下例子:
1
2
3
4
5
6
7
function buildUrl(){
    var qs = '?debug=true'
    with(location){
        var url = href + qs
    }
    return url
}

没有块级作用域

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

console.log(i) // 10

想必很多人都对上面这种情况挺熟悉的,部分人一开始以为输出的i会等于undefined或者0;其实i为10,这时因为js没有块级作用域({}里有自己的作用域,不过ES6中新增了块级作用域)。

声明变量
使用var声明的变量会被自动添加到最接近的环境中。在with语句中,最接近的环境是函数环境,所以也能理解上面延长作用域链的代码例子了。

1
2
3
4
5
6
function add(num1, num2){
    var sum = num1 + num2
    return sum
}
var result = add(10,20)
console.log(sum) // sum is not defined

如果初始化变量时没有使用var声明,该变量就会自动被添加到全局变量。

1
2
3
4
5
6
function add(num1, num2){
    sum = num1 + num2
    return sum
}
var result = add(10,20)
console.log(sum) // 30

如上,sum能够被访问到了。

注意,不声明而直接初始化变量是一个错误的做法,可能会导致一些问题

查询标识符
沿着作用域链向上查询,直至找到该标识符。最后找到全局环境都没找到,那就是该变量尚未声明。

垃圾收集

JS具有自动垃圾收集机制,即我们不需要关心内存的使用问题,所需内存的分配以及无用内存的回收实现了自动管理。垃圾收集机制会按照固定时间(或代码设定的时间)周期性执行这一操作。
在浏览器中,这一操作的策略有两种:

  1. 标记清除
    这是js最常用的垃圾收集方式。通过给变量标记的方式,判断是否从内存中删除它,从而完成内存清理工作。
  2. 引用计数
    对变量被引用的次数进行计数,但次数变成0时,就将其所占的内存空间回收起来。这种方法不太常用的原因是:它会遇到循环引用(即两个对象互相引用的情况)这个问题。

当然,为了优化页面的性能,减少占用内存,我们可以为不再使用的变量值设置为null,以便垃圾收集机制可以将其回收,这叫做解除引用