你不知道的JavaScript中

第一部分 类型和语法

1.类型

a.内置类型

JavaScript中变量是没有类型的,只有值才有类型,变量可以持有任何类型的值。
JS中有七种内置类型:

null undefined boolean number string object symbol

可使用typeof 判断类型,特殊的有:

typeof null // ==> "object"
typeof function(){}    // ==> "function"  属于object的子类型
typeof [] // ==> "object" 属于object的子类型
b.typeof Undeclared

看如下代码:

let a;
console.log(typeof a);      // undefined
console.log(typeof b);      // undefined        虽然b是一个undeclared的变量,但不报错

这里涉及到typeof的一个特殊安全防范机制,又时在判断一个可能未声明的变量是否有值时,非常有用:

// ReferenceError
if(DEBUG){
    console.log("debugging is starting");
}


// 这样是安全的
if(typeof DEBUG !=="undefined"){
    console.log("debugging is starting");
}

2.值

a.数组

js中的数组时特殊的js对象,使用delete运算符将单元从数组中删除后,数组length并不会发生变化。
在给数组直接添加数字索引时,数组索引大小大于长度时,数组length会变成添加索引的数字加一,而添加字符索引,则不会影响length

var arr = [];
arr[99] = 'hello';
arr.name = 'arr';
console.log(arr.length); // 100
console.log(arr)        // (100) [空 ã99, 'hello']
b.类数组

常见的类数组:arguments参数对象,DOM查询操作返回的列表,将类数组转成数组,可使用一些数组方法:

function foo (){
    var arr = Array.prototype.slice.call(arguments);
    arr.push('bam');
    console.log(arr);
}
foo('jfks','fsssdd')
c.字符串

JS中的字符串是不可变的,而数组是可变的,字符串是类数组,又是使用字符串借用数组的方法,可以达到比较好的效果:

console.log(Array.prototype.map.call('jiang', x => x.charCodeAt(0)));    // [106, 105, 97, 110, 103]
console.log(Array.prototype.join.call('jiang', '-'));    // j-i-a-n-g
d.数字

js中小数比较时,可以看到:0.1 + 0.2 == 0.3 为false,这是因为JS遵循的时IEEE754规范,怎样判断0.1 + 0.2 == 0.3呢,需要设置一个误差范围(也称"机器精度",mechine epsilon),才能比较,从ES6开始,该值定义在Number.EPSILON中,然后就可以判断两个小数的大小是否在误差范围内了:

console.log(Number.EPSILON);
// 在ES6 之前的版本写polyfill
if (!Number.EPSILON) Number.EPSILON = Math.pow(2, -52);

function numbersCloseEnoughToEqual(n1, n2) {
    return Math.abs(n1 - n2) < Number.EPSILON;
}
console.log(numbersCloseEnoughToEqual(0.1 + 0.2, 0.3));
e.void运算符

undeined时一个内置标识符,它的值为undefined,通过void运算发即可得到该值。表达式 void _ 没有返回值,因此返回结果时undefined,void并不改变表达式的结果,只是让表达式不返回值: void 42 ==> undefined

f.不是数字的数字

NaN意指"不是一个数字"(not a number),但将它理解为"无效的数字"可能会更准确一些。
对NaN的判断:

var a = 2 / "jf";
console.log(typeof a);

console.log(window.isNaN(a));       // true
console.log(window.isNaN('jiang'));     // true
// window.isNaN(..);     有缺陷


// ES6之前的Number.isNaN()的polyfill如下
if (typeof Number.isNaN !== 'function') {
    Number.isNaN = function (value) {
        // return typeof value === 'number' && window.isNaN(value);
        return value !== value;
    };
}

3.原生函数

a.常用的原生函数

内置原生函数:

String()、Number()、Boolean()、Array()、Object()、Function()、RegExp()、Date()、Error()、Symbol()
b.内部属性[[class]]

所有typeof 返回值为"object"的对象(如数组)都包含一个内部属性[[class]],我们可以看作是对object类型的进一步分类,这个属性无法直接获取,一般通过Object.prototype.toString(..)来查看:

Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(function() {});  // "[object Function]"
Object.prototype.toString.call(null);   // "[object Null]"
Object.prototype.toString.call(undefined);  // "[object Undefined]"

如果想得到封装对象中的基本类型值,可以使用valueOf()函数:new String('abc').valueOf() ==> 'abc'

c.原生函数Array(..)

当Array构造函数只带一个数组参数时,该参数会被作为数组的预设长度,使用new Array创建空数组:

new Array(6) //[空 ã6] 不建议
console.log(new Array(6).fill(undefined));
// 使用apply方法,生成一个新的数组,并填充undefined
console.log(Array.apply(null, {
    length: 6
}));

// apply方法的第二个参数是一个类数组或数组对象
// apply 通过类似于for循环依次获取这个类数组的每个元素,并将它们作为参数传递给函数 arr[0], arr[1], arr[2]
// 但是这个类数组只有length属性,不包含其他属性,arr[0] = undefined,
// 所以Array.apply(null, {length: 6}) 等同于 new Array(undefined, undefined, undefined, undefined, undefined, undefined)

4.强制类型转换

a.抽象值操作
I.ToString

抽象操作ToString,她负责处理非字符串到字符串的强制类型转换。
对基本类型来说,null转为"null"、undefined转为"undefined"、true转为"true"。
对于普通对象来说,除非自行定义,否则toString()(Object.prototype.toString())返回内部属性[[objct Object]]。
使用toString:

console.log({}.toString()); // [object Object]
console.log([2,3,4].toString()); // 2,3,4   数组重写了toString方法
console.log(new Date().toString()); // Wed Dec 31 1969 16:00:00 GMT+0800 (中国标准时间)  日期重写了toString方法

JSON字符串格式化
工具函数JSON.stringify(..)在将对象序列化成字符串时也用到了ToString。JSON字符串格式化与toString()的效果基本相同。所有安装的JSON值都可以使用JSON.stringify(..)字符串化,不安全的JSON值有:undefined、function、symbol和包含循环引用的对象都不符合JSON结构标准。JSON.stringify(..)在对象中遇到undefined、function、symbol时会自动将其忽略,在在数组中则会返回null(以保证单元位置不变)。

console.log(JSON.stringify(undefined));     // undefined
console.log(JSON.stringify(function () { }));  // undefined
console.log(JSON.stringify(Symbol('jiang')));  // undefined
console.log(JSON.stringify([1, undefined, function () { }, 4]));  // [1,null,null,4]

console.log(JSON.stringify({
    name: 'jiang',
    hello: function () {
        console.log('hello');
    }
}));      // {"name":"jiang"} 忽略函数

如果对象中定义了toJSON()方法,JSON字符串化时后首先调用该方法,然后用它的返回值来进行序列化。定义一个toJSON()方法来返回一个安全的JSON值。

 var o = {};
var a = {
    b: 42,
    c: o,
    d: function () { }
}
// 在 a 中创建一个循环引用
o.e = a;

// 循环引用会在这里产生错误
// JSON.stringify(a);

// 自定义的JSON序列化
a.toJSON = function () {
    return {
        b: this.b,
    }
}
console.log(JSON.stringify(a));  //{"b":42}

JSON.stringify(..)第二个参数,可以传入一个数组,数组的元素,则表示了需要被JSON字符串化的属性,也可以传入一个函数,他会对对象自生调用一次,然后对对象的每个属性各调用一次,每次调都将传入键和值;如果忽略某个键就返回undefined,否则就返回指定的值。
JSON.stringigy(..)第三个参数为,序列化后没级的缩进字符数

var a = {
    name: "홍길동",
    age: 20,
    sex: 1
}
console.log(
    JSON.stringify(a,
        (k, v) => {
            if (k !== "sex") return v;
        },
        2)
)
II.toNumber

其中特殊转换true==>1,false==>0,undefined==>NaN,null==>0;
对象(包括数组)会首先转换成相应的基本类型值,如果时非数字的基本类型值,则按上方特殊转换处理。
将值转换成镶银的基本类型操作ToPrimitive会首先(通过内部操作DefaultValue)检查该值是否有valueOf()方法。如果有且返回基本类型的值,则就使用该值进行强制类型转换。如果没有就是用toString的返回值(如果存在)进行强制类型转换。如果valueOf()和toString()均不返回基本类型的值,就会产生TypeError错误。

var a = {
    valueOf: function () {
        return 1;
    }
}
console.log(1 + a); // 2

var b = {
    toString: function () {
        return '99';
    }
}
console.log(1 + Number(b)); // 100

var c  = [1,2,3];
c.toString = function(){
    return this.join('');
}
console.log(Number(c)); // 123
III.ToBoolean

出了下列假值:
undefined、null、false、+0、-0、NaN、""、document.all(假值对象,用于判断是否为IE)
出了以上假值,在JS中的一般都为真值

b.字位运算符的妙用

首先要清楚,在JS中无论是整数还是小数都是按照64位的浮点数形式存储,字位运算符只能操作32位整数,运算符会强制使用32位格式,这是通过抽象操作ToInt32来实现的,ToInt32首先执行ToNumber强制类型转换,比如"123"会被先转成123,再执行ToInt32.
这里介绍两个常用的位运算符,位运算符使用参考:http://c.biancheng.net/view/5...

  //    | 位或运算符,直接用来取整  32.32 首先执行ToInt32(),然后执行|运算符(与0或运算得到本身),打印时执行ToString()
console.log(32.32 | 0); // 32

//    ~ 位非运算符,直接用来取整  32.32 首先执行ToInt32(),然后执行两次~运算符(得到本身),打印时执行ToString()
console.log(~~32.32); // -32

// ~x 的值,等同于 -(x + 1)  当x=-1时,等同于0,当它与indexOf(..)配合使用,就当未找到时,就假
var str = 'hello world';
console.log(!!~str.indexOf('q')); // 4
c.显示解析数字字符串

parseInt函数的使用,允许字符串中包含非数字字符,解析按从左至右,如果遇到非数字字符就停止,parseInt的第一个参数应当是字符串,当时其它类型的值是,会先将其强转为字符串类型,再作字符串解析:

parseInt(1/0,19)    // 18 (1/0 ==> Infinity   I为19进制中的有效值 为18)
parseInt(false,16)  // 250 (fa ==> false 前两位都是16进制中的有效值)
parseInt(parseInt,16) // 15 (f ==> function.. ==>parseInt)
d.隐式强转
I.字符串和数字之间的隐式强转

a + "" 与String(a)之间的区别,根据ToPrimitive抽象操作规则,a + "" 会先对a调用valueOf()方法,返回值非基本类型时,再调用toStirng()方法,然后通过ToString抽象操作将返回值转换成字符串,而String(a)则是直接调用ToString()。当a是对象而非数字时,这两者是有区别的:

var a = {
    valueOf:function(){
        return 2;
    },
    toString:function(){
        return 42;
    }
}

a + ""  // 2
String(a) // 42
II.||和&&

逻辑运算符||(或)和&&(与),但在JS中,把它称之为"操作数选择器运算符"恰当些,因为在JS中,这两个操作符的返回值并一定是布尔值,而是操作数中的一个。
||运算符,从左至右,返回第一个能转化为boolean值中为true的操作数,当全为false时,则返回最后一个数:

console.log(0 || false || '' || 88 || true) // 88
console.log(0 || false || '' || undefined || null) // null
a = a || 'a'    // 当a 为设置值,或为false 时, a = 'a',a为truth时,发生短路

&&运算符,从左至右,返回第一个能转化为boolean值中为false的操作数,当全为true时,则返回最后一个数:

console.log(true && [] && document.all && {}) // document.all(假值对象)
console.log({} && [] && true)   // true
a && foo()    // 当a为falsy时,发生短路,foo不会执行
III.符号类型的强制转换

在转换符号时,符号不能隐式转换成字符串,但是显示可强制转换成字符串:

// console.log(Symbol('hello') + '') // 报错
console.log(String(Symbol('hello'))) // Symbol(hello)

符号类型不能强制转换成数字(显示与隐式都会报错),当转成boolean值时,永远都是true。

IIII.宽松相等于严格相等

"=="与"==="之间的却别,"=="允许在比较相等中进行强制类型转换,而"==="不允许。
非常规情况:NaN 不等于 NaN,+0 等于 -0.
不同类型之间比较(==):

字符串于数字比较,先将字符串强转成数字后再进行比较
非布尔类型于布尔类型比较,先将布尔类型强转成数字后(1、0)进行比较
undefined == null,它们也与自生相等,其他的值不与它们相等
对象与非对象比较,先将对象执行ToPromitive抽象操作后,再进行比较

练习(提示:使用Object封装,对封装成原始值的对应封装类型):

console.log(42 == "42");        // true(ToNumber("42") ==> 42)
console.log(42 == true);        // false(ToBoolean(true) ==> 1)
console.log(undefined == null)  // true(不用转换,直接相等)
console.log(null == Object(null)) // 和Object()一样
console.log(undefined == Object(undefined)) // 和Object()一样
console.log(NaN == Object(NaN)) // 和new Number(NaN)一样
// null、undefined由于没有对应的封装对象,所以不能被封装, new Object(null)、new Object(undefined)均返回一个常规对象,与原始值不等
// NaN可以封装为数字封装对象,但拆封后NaN == NaN 为false

5.语法

a.运算符优先级

,运算符在表达式中优先级最低,&&的优先级比||优先级高,||的优先级高于?:运算符,且?:是一个右联合运算符。
练习:

var a = 42;
var b = 'foo';
var c = false;
var d = a && b || c ? c || b ? a : c && b : a;
//   var d = ((a && b) || c) ? ((c || b) ? a : (c && b)) : a;
//         'foo' ? ('foo' ? 42 :false) : 42;
// 42
b.自动分号插入(Automatic Semicolon Insetion,ASI)

如果JavaScript解析器发现代码行可能因为缺失分号而导致错误,那么它就会自动补上分号,并且,只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时,它才会这样做。代码语句块结尾的不需要加分号,所有for循环,while循环语句块不需要加;但时do..while循环需要加;,而我们在编写代码时,应当对ASI的依赖降到最低。


小江不可求
13 声望1 粉丝