前言

最近要入坑React Native,在廖雪峰的博客上找到一篇非常好的JavaScript入门教程。这篇文章是笔者在阅读廖老师的教程时做的笔记,摘抄了一些JavaScript区别与其他语言的特性和新手容易跳入的大坑。想详细阅读JavaScript入门教程的话,请访问JavaScript入门教程

自动加分号

  • JavaScript的语法和Java语言类似,每个语句以;结束,语句块用{…}。但是,JavaScript并不强制要求在每个语句的结尾加;,浏览器中负责执行JavaScript代码的引擎会自动在每个语句的结尾补上;。
    让JavaScript引擎自动加分号在某些情况下会改变程序的语义,导致运行结果与期望不一致。 比如:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function foo() {
    return
        { name: 'foo' };
}

foo(); // undefined

// 上面的代码等同于
function foo() {
    return; // 自动添加了分号,相当于return undefined;
        { name: 'foo' }; // 这行语句已经没法执行到了
}

// 正确的写法
function foo() {
    return { // 这里不会自动加分号,因为{表示语句尚未结束
        name: 'foo'
    };
}

Number

  • JavaScript不区分整数和浮点数,统一用Number表示,以下都是合法的Number类型
1
2
3
4
5
6
123; // 整数123
0.456; // 浮点数0.456
1.2345e3; // 科学计数法表示1.2345x1000,等同于1234.5
-99; // 负数
NaN; // NaN表示Not a Number,当无法计算结果时用NaN表示
Infinity; // Infinity表示无限大,当数值超过了JavaScript的Number所能表示的最大值时,就表示为Infinity

== 和 ===

”==” 比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;
”===” 比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。
由于JavaScript这个设计缺陷,不要使用\==比较,始终坚持使用===比较。 NaN这个特殊的Number与所有其他值都不相等,包括它自己

1
NaN === NaN; // false

唯一能判断NaN的方法是通过isNaN()函数:

1
isNaN(NaN); // true

null和undefined

  • null表示一个“空”的值,它和0以及空字符串”不同,0是一个数值,”表示长度为0的字符串,而null表示“空”。

  • 在其他语言中,也有类似JavaScript的null的表示,例如Java也用null,Swift用nil,Python用None表示。但是,在JavaScript中,还有一个和null类似的undefined,它表示“未定义”。

  • JavaScript的设计者希望用null表示一个空的值,而undefined表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用null。undefined仅仅在判断函数参数是否传递的情况下有用。

数组

数组是一组按顺序排列的集合,集合的每个值称为元素。JavaScript的数组可以包括任意数据类型。

  • 索引超出了范围,返回undefined
1
2
3
4
var arr = [1, 2, 3.14, 'Hello', null, true];
arr[0]; // 返回索引为0的元素,即1
arr[5]; // 返回索引为5的元素,即true
arr[6]; // 索引超出了范围,返回undefined
  • 如果通过索引赋值时,索引超过了范围,同样会引起Array大小的变化
1
2
3
var arr = [1, 2, 3];
arr[5] = 'x';
arr; // arr变为[1, 2, 3, undefined, undefined, 'x']

大多数其他编程语言不允许直接改变数组的大小,越界访问索引会报错。然而,JavaScript的Array却不会有任何错误。在编写代码时,不建议直接修改Array的大小,访问索引时要确保索引不会越界。

  • 使用new Array()创建数组需要注意
1
2
new Array(1, 2, 3); // 创建了数组[1, 2, 3]
new Array(2); // 创建了数组[undefined,undefined]
  • 直接给Array的length赋一个新的值会导致Array大小的变化
1
2
3
4
5
6
var arr = [1, 2, 3];
arr.length; // 3
arr.length = 6;
arr; // arr变为[1, 2, 3, undefined, undefined, undefined]
arr.length = 2;
arr; // arr变为[1, 2]

全局变量

JavaScript在设计之初,为了方便初学者学习,并不强制要求用var申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var申明就被使用,那么该变量就自动被申明为全局变量

1
2
i = 10; // i现在是全局变量
// 如果其他页面都不用var申明,恰好都使用了变量i,将造成变量i互相影响,产生难以调试的错误结果

字符串

字符串是不可变的,如果对字符串的某个索引赋值,不会有任何错误,但是,也没有任何效果

1
2
3
var s = 'Test';
s[0] = 'X';
alert(s); // s仍然为'Test'

对象

JavaScript用一个{…}表示一个对象,键值对以xxx: xxx形式申明,用,隔开。

  • 最后一个键值对不需要在末尾加,,如果加了,有的浏览器(如低版本的IE)将报错。

  • 如果in判断一个属性存在,这个属性不一定是xiaoming的,它可能是xiaoming继承得到的

1
'toString' in xiaoming; // true

因为toString定义在object对象中,而所有对象最终都会在原型链上指向object,所以xiaoming也拥有toString属性。 要判断一个属性是否是xiaoming自身拥有的,而不是继承得到的,可以用hasOwnProperty()方法:

1
2
3
4
5
var xiaoming = {
    name: '小明'
};
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false
  • number、string、boolean、function和undefined有别于其他类型。特别注意null的类型是object,Array的类型也是object,如果我们用typeof将无法区分出null、Array和通常意义上的object——{}
1
2
3
4
5
6
7
8
9
typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'
  • number、boolean和string都有包装对象
    虽然包装对象看上去和原来的值一模一样,显示出来也是一模一样,但他们的类型已经变为object了!所以,包装对象和原始值用===比较会返回false
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
typeof new Number(123); // 'object'
new Number(123) === 123; // false

typeof new Boolean(true); // 'object'
new Boolean(true) === true; // false

typeof new String('str'); // 'object'
new String('str') === 'str'; // false

// 如果不用New创建
// Number()、Boolean和String()被当做普通函数,把任何类型的数据转换为number、boolean和string类型(注意不是其包装类型)
var n = Number('123'); // 123,相当于parseInt()或parseFloat()
typeof n; // 'number'

var b = Boolean('true'); // true
typeof b; // 'boolean'

var b2 = Boolean('false'); // true! 'false'字符串转换结果为true!因为它是非空字符串!
var b3 = Boolean(''); // false

var s = String(123.45); // '123.45'
typeof s; // 'string'

总结:

  1. 不要使用new Number()、new Boolean()、new String()创建包装对象;
  2. 用parseInt()或parseFloat()来转换任意类型到number;
  3. 用String()来转换任意类型到string,或者直接调用某个对象的toString()方法;
  4. 通常不必把任意类型转换为boolean再判断,因为可以直接写if (myVar) {…};
  5. typeof操作符可以判断出number、boolean、string、function和undefined;
  6. 判断Array要使用Array.isArray(arr);
  7. 判断null请使用myVar === null;
  8. 判断某个全局变量是否存在用typeof window.myVar === ‘undefined’;
  9. 函数内部判断某个变量是否存在用typeof myVar === ‘undefined’;
  10. null和undefined没有toString()方法;
  11. number对象调用toString()报SyntaxError。
1
2
3
123.toString(); // SyntaxError
123..toString(); // '123', 注意是两个点!
(123).toString(); // '123'

循环

  • for … in对Array的循环得到的是String而不是Number
1
2
3
4
5
var a = ['A', 'B', 'C'];
for (var i in a) {
    console.log(i); // '0', '1', '2'
    console.log(a[i]); // 'A', 'B', 'C'
}
  • for … in循环由于历史遗留问题,它遍历的实际上是对象的属性名称。一个Array数组实际上也是一个对象,它的每个元素的索引被视为一个属性。

当我们手动给Array对象添加了额外的属性后,for … in循环将带来意想不到的意外效果

1
2
3
4
5
6
7
8
var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x in a) {
    console.log(x); // '0', '1', '2', 'name'
}
for (var x of a) {
    console.log(x); // 'A', 'B', 'C'
}

函数

  • 允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,传入的参数比定义的少也没有问题
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var abs = function (x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
};

abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
abs(); // 返回NaN
  • 函数不定义任何参数,还是通过arguments可以拿到参数的值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function abs() {
    if (arguments.length === 0) {
        return 0;
    }
    var x = arguments[0];
    return x >= 0 ? x : -x;
}

abs(); // 0
abs(10); // 10
abs(-9); // 9
  • 函数末尾如果没有return,就是隐含的return undefined;
1
2
3
4
5
function foo(x) {
    console.log(x)
}

var r = foo(1); // 调用foo函数, r 为undefined

变量提升

JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部

1
2
3
4
5
6
7
8
'use strict';

function foo() {
    var x = 'Hello, ' + y; //不报错,原因是变量y在稍后申明了
    console.log(x);
    var y = 'Bob';
}
foo();

对于上述foo()函数,JavaScript引擎看到的代码相当于:

1
2
3
4
5
6
function foo() {
    var y; // 提升变量y的申明,此时y为undefined
    var x = 'Hello, ' + y;
    console.log(x);
    y = 'Bob';
}

由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。

全局作用域

不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性。

1
2
3
4
5
6
7
'use strict';

var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'

// 直接访问全局变量course和访问window.course是完全一样的

JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误。

名字空间

全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。

减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 唯一的全局变量MYAPP:
var MYAPP = {};

// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 其他函数:
MYAPP.foo = function () {
    return 'foo';
};

局部作用域

由于JavaScript的变量作用域实际上是函数内部,我们在for循环等语句块中是无法定义具有局部作用域的变量的

1
2
3
4
5
6
7
8
'use strict';

function foo() {
    for (var i=0; i<100; i++) {
        //
    }
    i += 100; // 仍然可以引用变量i
}

为了解决块级作用域,ES6引入了新的关键字let,用let替代var可以申明一个块级作用域的变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
'use strict';

function foo() {
    var sum = 0;
    for (let i=0; i<100; i++) {
        sum += i;
    }
    // SyntaxError:
    i += 1;
}

this

  • 如果以对象的方法形式调用,比如xiaoming.age(),该函数的this指向被调用的对象,也就是xiaoming,这是符合我们预期的。

  • 如果单独调用函数,比如getAge(),此时,该函数的this指向全局对象,也就是window。

  • 如果这么写,也是不行的!要保证this指向正确,必须用obj.xxx()的形式调用!

1
2
var fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN
  • ECMA决定,在strict模式下让函数的this指向undefined,因此,在strict模式下,你会得到一个错误:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
'use strict';

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        var y = new Date().getFullYear();
        return y - this.birth;
    }
};

var fn = xiaoming.age;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined
  • 在函数内部定义的函数,this又指向undefined了!(在非strict模式下,它重新指向全局对象window!)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
'use strict';

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        function getAgeFromBirth() {
            var y = new Date().getFullYear();
            return y - this.birth;
        }
        return getAgeFromBirth();
    }
};

xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined

解决办法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
'use strict';

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        var that = this; // 在方法内部一开始就捕获this
        function getAgeFromBirth() {
            var y = new Date().getFullYear();
            return y - that.birth; // 用that而不是this
        }
        return getAgeFromBirth();
    }
};

xiaoming.age(); // 25

sort排序

  • 默认把所有元素先转换为String再排序
1
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]

闭包

  • 返回函数时不要引用任何循环变量,或者后续会发生变化的变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push(function () {
            return i * i;
        });
    }
    return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); //16 ,而不是 1
f2(); //16 ,而不是 4
f3(); //16 ,而不是 9
// 

原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了4,因此最终结果为16。
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push((function (n) {
            return function () {
                return n * n;
            }
        })(i));
    }
    return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

f1(); // 1
f2(); // 4
f3(); // 9

箭头函数

  • 如果参数不是一个,就需要用括号()括起来:
1
(x, y) => x * x + y * y
  • 如果要返回一个对象,需要用括号()括起来:
1
2
3
4
5
// SyntaxError:
x => { foo: x }

// ok:
x => ({ foo: x })
  • 箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定
1
2
3
4
5
6
7
8
9
var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
        return fn();
    }
};
obj.getAge(); // 25
  • 由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略
1
2
3
4
5
6
7
8
9
var obj = {
    birth: 1990,
    getAge: function (year) {
        var b = this.birth; // 1990
        var fn = (y) => y - this.birth; // this.birth仍是1990
        return fn.call({birth:2000}, year);
    }
};
obj.getAge(2015); // 25

Date

  • JavaScript的Date对象月份值从0开始,牢记0=1月,1=2月,2=3月,……,11=12月。
1
2
3
4
var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
now.getFullYear(); // 2015, 年份
now.getMonth(); // 5, 月份,注意月份范围是0~11,5表示六月
  • 使用Date.parse()时传入的字符串使用实际月份01到12,转换为Date对象后getMonth()获取的月份值为0到11。
1
2
3
4
5
var d = Date.parse('2015-06-24T19:49:22.875+08:00');
d; // 1435146562875
var d1 = new Date(1435146562875);
d1; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
d1.getMonth(); // 5