JavaScript学习笔记-基本数据类型与内存

基本概念

ECMAScript标准

由ECMA-262定义的ECMAScript与Web浏览器没有依赖关系,这门语言并不包括输入输出定义。ECMA-262定义的只是这门语言的基础,Web浏览器只是ECMAScript实现可能的宿主环境之一。宿主环境不仅提供基本的ECMAScript实现,同时也会提供该语言的扩展,以便语言与环境之间对接交互。而这些扩展——如DOM,则利用ECMAScript的核心类型和语法提供更多更具体的功能,以便实现针对环境的操作。其他的宿主环境包括Node(一种服务端JavaScript平台)和Adobe Flash。

ECMA-262规定的语言组成部分包括语法、类型、语句、关键字、保留字、操作符和对象。一个完整的JavaScirpt实现应当包括:

  • 核心(ECMAScript),由ECMA-262定义,提供核心语言功能;
  • 文档对象模型(DOM),提供访问和操作网页内容的方法和接口;
  • 浏览器对象模型(BOM),提供与浏览器交互的方法和接口。

在HTML中使用JavaScript

  1. sync属性:异步下载外部脚本(避免脚本过大或无效产生的阻塞,影响其他页面生成),只对外部脚本文件有效。
  2. defer:表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。
  3. 在script标签内嵌入式写js时,不要在代码的任何部分写</script>,否则代码会失效,需将/转义成\/
  4. 使用src属性引入外部脚本后,不能在标签内部再写其他代码。
  5. 现代Web应用一般把外部script标签放在body最后,以避免加载js时body不渲染(这也是页面优化的23条原则之一)。
  6. 对于js,没有硬性规定非要外部引用。但外部引用有好处,就是维护性强、可以缓存。

异步加载

  1. 在script标签中设置defer="defer",告诉浏览器立即下载外部脚本,但延迟执行。在实际中,延迟的脚本未必会按照原先的顺序执行,因此最好只包含一个延迟脚本。
  2. 与defer类似,async属性也有这样的作用。但完全不保证执行顺序。
  3. 当前浏览器不支持或禁用了js时,可以用noscript标签判断并警告。

语法与变量

基本语法

  1. ECMAScript的一切(如变量、函数名和操作符)都区分大小写。ECMAScript标识符采用驼峰式大小写格式,第一个字符必须是字母、下划线或美元符号。
  2. ECMAScript 5引入了严格模式(strict mode)的概念。严格模式下,ECMAScript中的一些不确定的行为将得到处理,而且对某些不安全的操作也会抛出错误。要在整个脚本中启用严格模式,在顶部添加"use strict",在函数内部上方包含这条编译指示,也可以指定函数在严格模式下执行。
  3. 语句的结尾可以有分号,也可以没有。但强烈建议使用分号结尾,否则将由解释器动态决定语句的域,大大增加性能开销。
  4. ECMAScript语法基本和C一致。但是label标志没有goto语句,只能用在break或continue后面。
  5. 无须指定函数的返回值,因为任何ECMAScript函数都可以在任何时候返回任何值。实际上,未指定返回值的函数返回的是一个特殊的undefined值。
  6. ECMAScript中也没有函数签名的概念,因为其函数参数是以一个包含零或多个值的数组的形式传递的。可以向ECMAScript函数传递任意数量的参数,并且可以通过arguments对象来访问这些参数。由于不存在函数签名的特性,ECMAScript函数不能重载。

变量定义

ECMAScript的变量是松散类型的,使用var操作符定义变量,省略var操作符会创建一个全局变量:

1
2
3
4
function test() {
var hello = "hello"; // 局部自动变量
message = "hi"; // 全局变量
}

数据类型

ECMAScript是松散类型的,规定了五种基础数据类型:undefined、NULL、number、string、boolean,和一个复杂数据类型Object。

操作符typeof

  1. typeof只会返回如下结果:undefined、boolean、string、number、object、function。
  2. null是空对象的引用,所以typeof null == object
  3. 如果一个变量声明了,但没初始化,它的类型就是undefined。实际上,undefined派生自null,所以(null == undefined) == true

Undefined类型

Undefined只有一个字面值,即undefined。使用var操作符声明一个对象而未进行显式初始化时,变量的值会被默认初始化为undefined。对未声明的变量执行typeof操作符也会返回undefined

即便未初始化的变量会自动被赋予undefined值,但显式地初始化变量依然是明智的选择。如果能够做到这一点,那么当typeof操作符返回undefined时,我们就知道被检测的变量没有被声明,而不是尚未初始化。

Null类型

Null类型是第二个只有一个值的数据类型,这个特殊值时null

Boolean类型

Boolean类型只有两个字面值:true和false。虽然Boolean类型字面值只有两个,但ECMAScript中所有类型的值都有与这两个Boolean值等价的值。要将一个值转换为其对应的Boolean值,可以调用转型函数Boolean()

1
2
var message = "Hello world!";
var messageAsBoolean = Boolean(message);

下面给出各数据类型及其转换的规则:
| 数据类型 | 转换为true的值 | 转换为false的值 |
| ————- | —————————————— | ———————- |
| Boolean | true | false |
| String | 任何非空字符串 | 空字符串 |
| Number | 任何非零数字值(包括无穷大) | 0 和 NaN |
| Object | 任何对象 | null |
| Undefined | n/a | undefined |

Number类型

ECMAScript中使用IEEE754格式来表示整数和浮点数值,因此ECMAScript并不能保存世界上所有的数值,它能够表示的最小数值保存在Number.MIN_VALUE中,对应的最大数值保存在Number.MAX_VALUE中。如果计算结果溢出,正数被转为Infinity,负数被转为-Infinity。想确定一个数是不是有穷的,可以使用isFinity()函数判断。

另外,永远不要比较两个浮点数是否相等。

NaN

NaN,即非数值(Not a Number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。例如,任何数值除以0会返回NaN。

NaN有两个非同寻常的特点:

  1. 任何涉及NaN的操作(如NaN / 10)都会返回NaN。
  2. NaN与任何值都不相等,包括NaN本身。可以用isNaN()确定这个参数是否不是数值。isNaN()在接收到一个值后,会尝试将这个值转换为数值。某些不是数值得值会直接转换为数值,例如字符串”10”或Boolean值,而任何不能被转换为数值的值都会导致这个函数返回true:
    1
    2
    3
    4
    5
    alert(isNaN(NaN)); // true
    alert(isNaN(10)); // false(10是一个数值)
    alert(isNaN("10")); // false(可以被转换成数值10)
    alert(isNaN("blue")); // false(不能被转换成数值)
    alert(isNaN(true)); // false(可以被转换成数值1)

Number类型转换

有三个函数可以把非数值转换为数值:Number()parseInt()parseFloat()
转型函数Number()可以用于任何数据类型,而另两个则专门用于把字符串转换成数值。

  1. 对于Number(),传入boolean返回1或0;传入null返回0;传入undefined返回NaN;传入字符串,前缀是数值,返回这个数值,不是就返回NaN,空字符串返回0;传入对象,先调用valueOf(),然后处理,如果结果是NaN,再调用toString()再处理一次。
  2. 对于parseInt(),可以有另外一个参数,就是进制,标明第一个参数是什么进制,然后统统转成10进制;其他和Number()类似,只不过parseInt()传入空字符串返回NaN。
  3. 对于parseFloat(),与parseInt()的区别是会保留一个小数点;第二个区别是只解析十进制,解析式会忽略前导的0。如果传入字符串没有小数点,全是数字,就返回整数。所以传入的16进制串都转成0。

String类型

String类型用于表示由零或多个16位Unicode字符组成的字符序列,即字符串。字符串可以由双引号或单引号表示,两者等价。

ECMAScript中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量。字符串的长度都可以通过访问其length属性取得。

把一个值转换为一个字符串有两种方式。一种是使用几乎每个值都有的toString()方法,在调用数值的toString()方法时,可以传递一个参数指定转换的进制;在不知道要转换的值是不是nullundefined的情况下,可以使用转型函数String()

Object类型

ECMAScript中的对象其实就是一组数据和功能的集合。对象可以通过执行new操作符创建,也可以通过初始化字面量的方法创建:

1
2
var newObject = new Object();
var myObject = {};

Object类型是所有它的实例的基础,Object的每个实例都具有下列属性和方法:

  1. constructor:保存着用于创建当前对象的函数。对于前面的例子而言,构造函数就是Object()
  2. hasOwnProperty(propertyName):用于检测给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。其中参数的属性名必须以字符串形式指定。

    1
    2
    3
    4
    var a = new Object();
    a.b = "b";
    alert(a.hasOwnProperty("constructor")); //false
    alert(a.hasOwnProperty("b")); //true
  3. isPrototypeOf(object):用于检查传入的对象是否是传入对象的原型。

  4. propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用for-in语句来枚举。与hasOwnProperty()方法一样,参数的属性名必须以字符串形式指定。
  5. toLocaleString():返回对象的字符串表示,该字符串与执行环境的地区对应。
  6. toString():返回对象的字符串表示。
  7. valueOf():返回对象的字符串、数值或布尔值表示,通常与toString()方法的返回值相同。

操作符引起的类型转换

  1. ECMAString所有数字用IEEE754 64位格式存储,但位操作并不是直接操作这64位,而是将这64位转换成32位整数,然后再操作,然后转换回来。存储的64位对程序员是透明的。
  2. ECMAString的逻辑运算是短路操作,比如连续与操作,如果第一个值是false,后面的条件就不判断了。
  3. 关于无穷一切不确定的结果,多半是NaN:

    1
    2
    3
    4
    5
    6
    7
    8
    Infinity * 0 = NaN;
    Infinity - Infinity = NaN;
    Infinity * Infinity = Infinity;
    Infinity * other = Infinity;
    Infinity / Infinity = NaN;
    (+Infinity) + (-Infinity) = NaN;
    (+0) + (-0) = (+0);
    (-0) - (-0) = (+0);
  4. 加号操作,如果一个操作数是string,那么其他类型的操作数会自动调用String转换成字符串,然后拼串。

  5. 减号、乘号、除号,如果操作数中有string、boolean、null或unsigned,先自动调用Number()把这些类型转换成数值,然后在进行减法运算。

关系操作

  1. 如果一个是number,另一个是非Object,则将另一转换成number然后比较;
  2. 如果一个是number,另一个是Object,则另一个调用valueOf,没有就调用toString。
  3. 如果一个是boolean,就先将其转换成number。
  4. 两个都是string,直接比较ASCII值:

    1
    2
    3
    4
    5
    ("Book" < "apple") == true; // 按ASCII码比较
    ("Book".toLowerCase() < "apple".toLowerCase()) == false; // 统一大小写比较
    ("23" < "3") == true; // 字符串比较
    ("23" < 3) == false; // 先转为Number类型再比较
  5. 操作数中有一个是NaN,比较大小返回false,比如"a" < 3,相当于Number("a") < 3,即NaN < 3 == false

  6. 操作数有一个是NaN,判断相等返回false,判断不相等返回true。
  7. 相等和不相等,一个操作数是string一个是number,调用Number();一个是Object一个是非Object,调用valueOf()

相等操作符

ECMAScript中的相等操作符由==表示,不等操作符!=表示。

在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则:

  • 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false转换为0,而true转换为1;
  • 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转为数值;
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则进行比较;

这两个操作符在进行比较时则要遵循下列规则:

  • nullundefined总是相等的。
  • 要比较相等性之前,不能将null和undefined转换成其他任何值。
  • 如果有一个操作数是NaN,则相等操作符返回false,而不相等操作符返回true。(即使两个操作数都是NaN)
  • 如果两个操作数都是对象,则比较他们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true;否则,返回false。
  • 全等===和不全等!==操作符在比较之前不转换操作数。由于相等和不相等操作符存在类型转换问题,而为了保持代码中数据类型的完整性
    ,推荐使用全等和不全等操作符。

执行环境与作用域链

值和引用

  1. 基础类型值:JavaScript中五个基础类型unsigned、null、number、boolean、string,这些类型的变量名代表值。基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中。从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本。(注意字符串也是基本数据类型,因此遵循值传递)

  2. 引用类型值:JavaScript中object、function、array、date(array和date都是object)等类型都是引用类型。引用类型变量的值是对象,保存在堆内存中。包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针。从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象。函数对应的形参类型也是引用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var a = 1, b = "str", c = [0, 1, 2], d = {e: "abc"};
    function changeValue(a, b, c, d) {
    a = 2;
    b = "str2";
    c[0] = 3;
    d.e = 789;
    }
    changeValue(a, b, c, d);
    console.log(a); // 1
    console.log(b); // str
    console.log(c); // 3,1,2
    console.log(d.e); // 789
  3. 确定一个值是哪种基本类型可以使用typeof操作符,而确定一个值是哪种引用类型可以使用instanceof操作符。

执行环境和作用域

所有变量(包括基本类型和引用类型)都存在于一个执行环境(也成为作用域)当中,这个执行环境决定了变量的生命周期,以及变量和函数有权访问的其他数据。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。

执行环境分为全局执行环境和函数执行环境,每次进入一个新执行环境,解释器都会创建一个用于搜索变量和函数的作用域链。第一个元素就是当前执行环境(function)中的局部变量,然后依次向外扩展,全局变量永远在最后一个。变量的寻找是按照名字,沿着作用域链一级一级回溯的。如果当前执行环境没有,就搜索下一级,直到搜索完毕。因此,在执行环境嵌套后,内层变量会覆盖外层同名变量,同名的局部变量对全局变量有屏蔽作用。

JavaScript没有块作用域,执行环境依据function(){}来界定。因此if、for、while语句都不能成为独立的作用域,尤其循环中的变量会绑定至当前函数的执行环境中,可以在循环外访问,需要特别注意。

作用域链是可以通try或with延长,但这样的做法不是best practice。

JavaScript中的局部变量应使用var操作符规范定义。如果不使用var定义变量,这个变量就会被添加到全局环境中,函数执行完之后还会继续存在。

垃圾回收

JavaScript是一门具有自动垃圾收集机制的编程语言,解释器会每隔一段时间自动清理一次。垃圾回收清理的依据一般有标志清除和引用次数等。

标记清除是目前主流的垃圾收集算法,离开作用域的值将被自动标记为可以回收,将在垃圾收集期间被删除。另一种垃圾收集算法是引用计数,这种算法的思想是跟踪记录所有制被引用的次数。为了避免循环引用问题(尤其是原生JavaScript对象与DOM对象之前的循环引用),当前的JavaScript引擎都不再使用这种算法。

在编写JavaScript代码时,主动将不再使用的变量引用置空,不仅有助于避免循环引用,也可以优化对垃圾回收时的性能。同时为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。