Pinvon's Blog

所见, 所闻, 所思, 所想

JavaScript笔记--语法

声明

本系列的笔记来自于: 阮一峰老师的JavaScript教程

基本语法

变量

变量的声明与赋值:

var a = 1;
a = 'hello';

如果不写 var, 程序也有效, 但这种做法会在不知不觉中创建全局变量.

注释

单行注释: / 多行注释: /* ... *

区块

对于 var 命令来说, JavaScript的区块不构成单独的作用域. 如:

{
    var a = 1;
}
console.log(a); // 1

if语句

与C++/Java相同.

switch语句

与C++/Java相同.

三目运算符

JavaScript中存在三目运算符.

循环

for, while, do...while 使用方法与C++/Java一样.

JavaScript有关键字 breakcontinue.

数据类型

JavaScript的数据类型有6种.

  • 数值(number): 整数和小数
  • 字符串(string): 文本
  • 布尔值(boolean)
  • undefined: 表示未定义
  • null: 空值
  • 对象(object): 各种值组成的集合

typeof运算符

JavaScript有三种方法可以确定一个值是什么类型: typedef运算符, instanceof运算符, Object.prototype.toString方法. 这边先介绍typeof运算符.

typeof 123;  //  "number"
typeof '123';  //  "string"
typeof false  //  "boolean"

function f() {}
typeof f  //  "function"

应用:

if (typeof v === "undefined") { ... }

布尔值

以下六个值在转换为布尔值时, 被转为false:

  • undefined
  • null
  • false
  • 0
  • NaN
  • ""或''

数值

整数和浮点数

在JavaScript内部, 所有数字都是以64位浮点数形式储存, 即使整数也是如此. 因此, 1和1.0是相同的:

1 === 1.0  // true

由于浮点数不是精确的值, 所以涉及小数的比较和运算, 需要特别小心:

0.1 + 0.2 === 0.3  //  false

数值精度

首先介绍 IEEE 754标准, 在64位的情况下:

  • 第1位: 符号位
  • 第2~12位(共11位): 指数部分
  • 第13~64位(共52位): 小数部分(即有效数字)

符号位决定一个数的正负, 指数部分决定数值大小, 小数部分决定数值精度.

指数部分共11个二进制位, 大小范围在0~2047之间. IEEE 754 规定, 如果指数部分的值在区间(0, 2047), 则小数部分的第1位默认是1, 且不保存在64位浮点数中. 所以, JavaScript提供的有效数字最长为53个二进制位. 即: $$(-1)^{符号位} \times 1.xx...xx \times 2^{指数部分}$$

精度最多只能到53位二进制位, 这意味着, 绝对值小于等于2的53次方的整数, 都可以精确表示. 大于2的53次方的数值, 无法保持精度.

数值范围

指数部分为11个二进制位, 可以表示的最大值是2047, 还要分一半表示负数, 因此, JavaScript能够表示的数值范围为 \(2^{1024}\) 到 \(2^{-1023}\) 之间(开区间), 超出这个范围的数无法表示.

数值表示法

十进制, 十六进制(0xFF), 科学计数法(123e3)

与数值相关的全局方法

  • parseInt(): 将字符串转成整数
  • parseFloat(): 将字符串转成浮点数
  • isNaN(): 判断是否为NaN
  • isFinite(): 判断是否为正常数值

字符串

字符串就是0个或多个排在一起的字符, 放在单引号或双引号之中.

单引号字符串内部, 可使用双引号; 双引号字符串内部, 可使用单引号.

字符串拼接使用+.

字符串与数组

字符串可以看成是数组, 因此可以使用数组的方括号运算符, 用来返回某个位置的字符, 索引从0开始. 无法改变字符串之中的单个字符. length 属性返回字符串长度.

var s = 'hello';
s[0]  //  "h"

s[1] = 'a';
s  //  "hello"

s.length  //  5

对象

简单说, 对象就是一组"键值对"的集合, 是一种无序的复合数据集合.

var obj = {
    foo: 'Hello',
    bar: 'World'
};

对象的每一个键名又称为"属性", 它的"键值"可以是任何数据类型. 如果一个属性的值为函数, 通常把这个属性称为"方法", 它可以像函数那样调用.

var obj = {
    p: function (x) {
        return 2 * x;
    }
};
obj.p(1)  // 2

如果属性的值是另一个对象, 就形成了链式引用:

var o1 = {};
var o2 = {bar: 'hello'};
o1.foo = o2;
o1.foo.bar  // "hello"

该例子还说明, 属性可以动态创建, 不必在对象声明时就指定.

对象的引用

如果把一个变量赋值为一个对象, 则它们都是这个对象的引用, 指向同一个内存地址, 修改其中一个变量, 会影响到其他所有变量. 如果把该变量又赋值为另一个值, 不会对原有对象造成影响.

属性的操作

读取属性

两种方法, 点运算符, 或方括号运算符.

var obj = {
    p: 'Hello World'
};
obj.p
obj['p']

注意: 属性名必须放在引号中间.

属性的赋值

var obj = {};
obj.foo = 'Hello';
obj['bar'] = 'World';

查看所有属性

Object.keys(obj);

delete命令

delete obj.p

in运算符

用于检查对象是否包含某个属性.

for ... in 循环

用来遍历一个对象的全部属性. 但有些属性是不可遍历的, 如toString在对象创建之初就存在, 但是不可遍历.

数组

数组是按次序排列的一组值, 下标从0开始. 数组的赋值方法:

// 法1
var arr = ['a', 'b', 'c'];

// 法2
var arr = [];
arr[0] = 'a';

任何类型的数据都可以放入数组:

var arr = [
    {a: 1},
    [1, 2, 3],
    function () { return true; }
];

数组的本质

数组本质上是一种特殊的对象. typeof 运算符会返回数组的类型是 object. 数组的特殊性在于, 它的键名(属性)是按次序排列的一组整数(0, 1, 2, ...). 如:

var arr = ['a', 'b', 'c'];
Object.keys(arr)  // ["0", "1", "2"]

Object.keys() 返回数组的所有键名.

JavaScript规定, 对象的键名一律为字符串, 所以, 数组的键名其实也是字符串, 之所以可以用数值读取, 是因为非字符串的键名会被转为字符串.

对象读取成员有两种方法: 点结构(object.key)和方括号结构(object[key]). 但是, 对于数值的键名, 不能使用点结构:

var arr = [1, 2, 3];
arr.0  // SyntaxError

length属性

数组的 length 属性, 返回数组的成员数量.

数组的 length 属性是动态可变的, 如果你想清空一个数组, 就把 length 属性设为0.

有一点需要注意, 数组的 length 属性是键名最大的数字加1, 这就需要数组的键名是整数. 如果我们把数组的键名改成字符串或小数, 则 length 属性保持不变:

var arr = [];

arr['p'] = 'abc';
a.length  // 0

a[2.1] = 'abc';
a.length  // 0

使用 delete 命令删除一个数组成员, 会形成空位, 不影响 length 的值:

var a = [1, 2, 3];
delete a[1];
a[1]  // undefined
a.length  // 3

遍历数组

遍历数组的键名, 使用: in

遍历数组的值, 使用: for ... in(不推荐), while, for, forEach

函数

函数声明

声明函数有三种方法.

function命令

function print (s) {
    console.log(s);
}

函数表达式

var print = function (s) {
    console.log(s);
};
print(1)

注意, 这种写法在函数表达式后面有分号结尾.

也可以在采用函数表达式声明函数时, function 命令后面带上函数名. 但是这个函数名只在函数内部有效, 在函数外部无效. 建议加上函数名, 因为这样可以在程序出错时, 通过函数调用栈定位.

var print = function x () {
    console.log(typeof x);
};
print()

Function构造函数

var add = new Function (
    'x',
    'y',
    'return x + y'
);
//  等同于
function add (x, y) {
    return x + y;
}

不推荐这种方法.

函数的重复声明

后面的声明会覆盖前面的声明.

函数作为参数

function add(x, y) {
    return x + y;
}
function a(op) {
    return op;
}
a(add)(1, 1)  // 2

函数的属性和方法

  • name: 返回函数的名字 (f1.name)
  • length: 返回参数个数 (f1.length)
  • toString(): 返回函数内部代码 (f1.toString())

参数

如果在定义时提供了两个参数, 而调用时使用了更多或更少参数, 都不会报错.

参数的传递方式

如果参数是原始数据类型(数值, 字符串, 布尔值), 则采用值传递, 这意味着, 在函数体内修改参数值, 不会影响到函数外部.

var p = 2;
function f(p) {
    p = 3;
}
f(p);
p  // 2

如果参数是复合类型的值(数组, 对象, 其他函数), 则采用地址传递, 也就是说, 传入函数的原始值的地址, 因此, 在函数内部修改参数, 会影响到原始值.

var obj = { p:1 };
function f(o) { o.p = 2; }
f(obj);
obj.p  // 2

注意, 如果函数内部修改的, 是不参数对象的某个属性, 而是替换掉整个参数, 则不会影响到原始值.

var obj = [1, 2, 3];
function f(o) { o = [2, 3, 4]; }
f(obj);
obj  // [1, 2, 3]

解析: 形式参数 o 的值一开始是参数 obj 的地址, 重新对 o 赋值, 会导致 o 指向另一个地址, 保存在原地址上的值当然不受影响.

arguments对象(可变参数个数)

arguments 对象包含了函数运行时的所有参数. 使用方法: arguments[index].

function f() {
    return arguments.length;
}
f(1, 2, 3)  // 3
f(1)  // 1

闭包

闭包的由来: 由于变量存在作用域, 所以函数内部可以直接读取全局变量, 而函数外部无法读取函数内部声明的变量. 要想在函数外部获得函数内部的局部变量, 正常情况下是办不到的, 但可以变通方法.

function f1() {
    var n = 999;
    function f2() {
        console.log(n);
    }
    return f2;
}

var result = f1();
result();  // 999

在上面的代码中, 函数 f1 的返回值就是函数 f2, 由于 f2 可以读取 f1 的内部变量, 所以就可以在外部获得 f1 的内部变量了.

闭包就是函数 f2, 即能够读取其他函数内部变量的函数. 在本质上, 闭包是把函数内部和外部连接起来的一座桥梁.

闭包的最大用处有两个, 一是可读取函数内部的变量, 二是让这些变量始终保持在内存中, 即闭包可以使它诞生环境一直存在. 如:

function createIncrementor(start) {
    return function() {
        return start++;
    };
}

var inc = createIncrementor(5);

inc()  // 5
inc()  // 6
inc()  // 7

通过闭包, start 的状态被保留了, 每一次调用都是在上一次调用的基础上进行计算.

因为 inc 始终在内存中, 而 inc 的存在依赖于 createIncrementor, 因此也始终在内存中, 不会在调用结束后, 被垃圾回收机制回收.

数据类型转换

强制转换

  • Number(): 强制转换为数值, 比 parseInt() 严格
  • String(): 将任意类型的值转化成字符串
  • Boolean(): 将任意类型的值转化为布尔值

错误处理机制

Error实例对象

JavaScript解析或运行时, 一旦发生错误, 引擎就会抛出一个错误对象. JavaScript原生提供Error构造函数, 所有抛出的错误都是这个构造函数的实例.

var err = new Error('出错了');
err.message  //  "出错了"

抛出Error实例对象后, 整个程序就中断在发生错误的地方, 不再往下执行.

原生错误类型

  • SyntaxError: 解析代码时发生的语法错误
  • ReferenceError: 引用一个不存在的变量时发生的错误
  • RangeError: 一个值超出有效范围时发生的错误
  • TypeError: 变量或参数不是预期类型时发生的错误
  • URIError: URI相关函数的参数不正确时抛出的错误
  • EvalError: eval函数没有被正确执行时抛出的错误

自定义错误

function UserError(message) {
    this.message = message || '默认信息';
    this.name = 'UserError';
}
UserError.prototype = new Error();  //  继承自Error对象
UserError.prototype.constructor = UserError;

// 用法
new UserError('这是自定义的错误');

throw语句

throw 的作用是手动中断程序执行, 抛出一个错误.

if(x < 0) {
    throw new Error('x 必须为正数');
}

对于JavaScript引擎来说, 遇到 throw 语句, 程序就中止了.

try ... catch 结构

try ... catch 结构允许对错误进行处理, 选择是否往下执行.

try {
    throw "出错了";
} catch(e) {
    console.log(111);
}
console.log(222);

try 代码块抛出的错误, 如果被 catch 代码块捕获后, 程序就会继续向下执行.

可以捕获不同类型的错误:

try {
    foo.bar();
} catch(e) {
    if(e instanceof EvalError) {
        console.log(e.name + ":" + e.message);
    } else if(e instanceof RangeError) {
        console.log(e.name + ":" + e.message);
    }
    // ...
}

finally 代码块

try ... catch 结构允许在最后添加一个 finally 代码块, 表示不管是否出现错误, 都必须在最后运行的语句, 即使在 try 代码块中有 return 语句, 也还会去执行 finally.

Comments

使用 Disqus 评论
comments powered by Disqus