《你不知道的JavaScript 中卷》

138 阅读9分钟

内置类型

数组也是对象。它也是Object的一个”子类型“,数组元素按数字顺序来进行索引(而非像普通对象那样通过字符串键值),其length属性是元素的个数。而函数的length是参数的个数

JavaScript中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值

typeof 运算符总是返回一个字符串(首字母小写)

变量在未持有值的时候为undefined

typeof处理undeclared变量的方式

var a 
typeof a // "undefined"
typeof b // "undefined"

对于undeclared(或者 not defined)变量,typeof照样返回”undefined“

类数组

一些DOM查询操作会返回DOM元素列表,它们并非真正意义上的数组,另个一个是通过arguments对象将函数的参数列表来访问

function foo(){
  var arr = Array.prototype.slice.call(arguments)
  arr.push("bam")
  console.log(arr)
}
foo("bar","baz") //["bar","baz","bam"]

Array.from 也能实现功能

var arr = Array.from(arguments)

字符串

var a = "foo"
var b =["f","o","o"]

a.length //3
b.length //3

a.indexOf("o") //1
b.indexOf("o") //1

var c = a.concat("bar") //"foobar"
var d = b.concat(["b","a","r"]) //["f","o","o","b","a","r"]
a === c //false
b === d //false

a //"foo"
b //["f","o","o"]

JavaScript中字符串是不可变的,而数组是可变的

许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过”借用“数组的非变更方法来处理字符串

a.join //undefined
a.map //undefined

var c =Array.prototype.join.call(a,"_")
var d =Array.prototype.map.call(a,function(v){
  return v.toUpperCase() + "."
}).join("")

c // "f-o-o"
d // "F.o.o."

先将字符串转换成数组,然后转会字符串

var c = a 
// 将a的值转换为字符串数组
.split("")
// 将数组中的字符进行倒转
.reverse()
// 将数组中的字符拼接回字符串
.join("")

如果经常需要处理字符串数组的话,倒不如直接使用数组

数字

由于数字值可以使用Number对象进行封装,因此数字值可以调用Number.prototype中的方法。

var a = 42.59
a.toFixed(0) //"43"
a.toFixed(1) //"42.6"
a.toFixed(2) //"42.59"
a.toFixed(3) //"42.590"
0.1 + 0.2 === 0.3 //false

二进制浮点数的0.1和0.2并不是十分的精确,它们相加的结果并非刚好等于0.3,而是比较接近的数字0.30000000000004

特殊的数值

不是值的值 undefined 类型只有一个值,即undefined。null类型也只有一个值,即null。它们的名称即使类型也是值

undefined 和 null 常被用来表示”空的“值,或”不是值“的值。

null 指空值
undefined 指没有值

undefined 指从未赋值
null 指曾赋过值 但目前没有值

NaN 不是一个数字 ----> 无效的值,失败的值,坏数值 NaN 是一个特殊的值,它和自身不相等。不能比较

NaN != NaN 为true

var a = 2 / "foo"
var b = "foo"

Number.isNaN(a)  // true
Number.isNaN(b)  // false

判断是不是NaN值尽量使用Number.isNaN()

值和引用

简单值总是通过值复制的方式来赋值/传递,包括null,undefined,字符串,数字,布尔,ES6中的symbol。

复合值--对象(包括数组和封装对象和函数),则总是通过引用复制的方式来赋值/传递

第三章

原生函数

第一章第二章曾提到JavaScript的内建函数,也叫原生函数(native function),如String和Number 常用的原生函数有:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol() -- ES6 中加入 内建函数 --- 不就是内置对象吗

原生函数可以被当作构造函数来使用,但其构造出来的对象可能会和我们设想的有所出入:

var a = new String("abc")
typeof a // 是"object",不是"String"
a instanceof String // true
Object.prototype.toString.call(a) // "[object String]"

通过构造函数(如 new String("abc"))创建出来的封装了基本类型值(如“abc”)的封装对象

是封装对象,而非基本类型值“abc”

内部属性[[Class]]

Object.prototype.toString.call([1,2,3])
Object.prototype.toString.call(/regex-literal/i)

数组的内部[[Class]]属性值是“Array”,正则表达式的值是“RegExp”

封装对象包装

由于基本类型值没有.length和.toString()这样的属性和方法,需要通过封装对象才能访问,此时JavaScript会自动为基本类型值包装(box或者wrap)一个对象

一般情况下,我们不需要直接使用封装对象。最好的办法是让JavaScript引擎自己决定什么时候应该使用封装对象。换句话说,就是应该优先考虑使用“abc”和42这样的基本类型值,而非new String("abc")和new Number(42)

要向得到封装对象种的基本类型值,可以使用valueof()函数

var a = new String("abc")
var b = new Number("42")
var c = new Boolean(true)

a.valueOf() //"abc"
b.valueOf() //42
c.valueOf() //true

强烈建议使用常量形式 (/^a*b+/g/) 来定义正则表达

原生原型

原生构造函数有自己的.prototype 对象,如Array.prototype,String.prototype等 这些对象包含其对应子类型所有特有的行为特征

第四章

强制类型转换

将值从一种类型转换为另一种类型通常称为类型转换,这是显示的情况;隐式的情况称为强制类型转换

JavaScript中的强制类型转换总是返回标量基本类型值,如字符串,数字和布尔值,不会返回对象和函数。

String 转换

JSON.stringify(undefined) //undefined
JSON.stringify(function(){}) //undefined

JSON.stringify([1,undefined,function(){},4]) //"[1,null,null,4]"
JOSN.stringify({a:2,b:function(){}}) // "{"a":2}"

字符串和数字之间的显式转换

var a = 42
var b = String(a)
var f = a.toString()

var c = "3.14"
var d = Number(c)
var e = + c

b //"42"
d //3.14

我们不建议对日期类型使用强制类型转换,应该使用Date.now()来获取当前的时间戳,使用new Date(..).getTime()来获取指定时间的时间戳

var a = "42"
var b = "42px"

Number(a) //42
parseInt(a) //42

Number(b) // NaN
parseInt(b) //42

解析字符串中的浮点数可以使用parseFloat()函数

所以显式强制类型转化为布尔值最常用的方法是!!,因为第二个!会将结果翻转回原值

&& 和 || 运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。

var a = 42
var b = "abc"
bar c = null

a || b //42
a && b // "abc"

c || b //"abc"
c && b // null

a || b;
// 大致相当于(roughly equivalent to): a ? a : b;
a && b;
// 大致相当于(roughly equivalent to): a ? b : a;

如果条件判断未通过,a && foo()就会悄然 终止(也叫作“短路”,short circuiting),foo() 不会被调用。

开发人员通常使用 if (a) { foo(); }。但 JavaScript 代码压缩工具用的是 a && foo()

既然返回的不是true和false,为什么a && (b || c)这样的表达式在 if 和 for 中没出过问题? 这或许并不是代码的问题,问题在于你可能不知道这些条件判断表达式最后还会执行布尔 值的隐式强制类型转换。

var a = 42; 
var b = null; 
var c = "foo";
if (a && (b || c)) { 
  console.log( "yep" );
}

这里a && (b || c)的结果实际上是"foo"而非true,然后再由if将foo强制类型转换为
布尔值,所以最后结果为 true。 

现在明白了吧,这里发生了隐式强制类型转换。如果要避免隐式强制类型转换就得这样:
if (!!a && (!!b || !!c)) { 
  console.log( "yep" );
}

抽象相等

var a = "42"
var b = true

a==b //false

因为true 的强制转换的结果是1

// 不要这样用,条件判断不成立: if (a == true) {
// .. }
// 也不要这样用,条件判断不成立: if (a === true) {
// .. }
// 这样的显式用法没问题: if (a) {
// .. }
// 这样的显式用法更好: if (!!a) {
// .. }
// 这样的显式用法也很好: if (Boolean( a )) {
// .. }

null 和undefined 之间的相等比较

var a = null
var b;
a == b //true
a == null // true
b == null //true

a == false //false
b == false //false

a == "" //false
b == "" //false

a == 0 //false
b == 0 //false

对象和费对象之间的相等比较

var a = 42
var b = [42]

a == b //true

调用ToPromitive抽象,返回的是字符串"42"

var a = "abc"
var a = new Object(a)

a === b //false
a == b //true
"0" == null; // false
"0" == undefined;  // false
"0" == false; // true -- 晕!
"0" == NaN; // false
"0" == 0; // true
"0" == ""; //false

false == null; // false
false == undefined;  // false
false == NaN; // false
false == 0; true -- 晕!
false == ""; true -- 晕!
false == []; true -- 晕!
false == {}; // false

"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; true -- 晕!
"" == []; true -- 晕!
"" == {}; // false

0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true
0 == {}; // false

安全运用隐式强制类型转换

* 如果两边的值中有true或者false,千万不要使用 == 
* 如果两边的值有[],"",或者0,尽量不要使用 ==

本章介绍了JavaScript的数据类型之间的转换,即强制类型装换:包括显示和隐式

function vowels(str){
  var matches
  if(str){
    matchs = str.match(/[aeiou]/g)
    if(matchs){
      return matches
    }
  }
}
vowels("Hello World") // ["e","o","o"]
function vowels(str){
  if(str && (matches = str.match([/[aeiou]/g]))){
    return matches
  }
}

第二部分

异步和性能

直到ES6,JavaScript才真正内建有直接的异步概念 JavaScript引擎并不是独立运行的,它运行在宿主环境中,对多数开发者通常来说就是web浏览器。但是现在JavaScript已经超出了浏览器的范围了。如在node中运行,

它们都有一个共同的点(thread,也指线程),即它们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用JavaScript引擎,这种机制被称为时间循环

JavaScript引擎会通知宿主环境,嘿,现在我要暂停执行,你一旦完成网络请求,拿到了数据,就请调用这个函数

然后浏览器会设置侦听来自网络的响应,拿到要给你的数据之后,就会把回调函数插入到事件循环,依次实现对这个回调的调度执行

事件循环

// eventLoop 是一个用作队的数组
// (先进,先出)
var eventLoop = []
var event

// "永远"执行
while(true){
  if(eventLoop.length > 0){
    // 拿到队列中的下一个事件
    event = eventLoop.shift();
    // 现在,执行下一个事件
    try {
      event()
    }
    catch(err){
      reportError(err)
    }
  }
}

前面提高的Es6从本质上改变了在哪管理事件循环。本来它几乎已经是一种正式的技术模型了,但现在ES6精确指定了事件循环的工作细节,这意味着在技术上将其纳入JavaScript引擎的势力范围,而不是只由宿主环境来管理。这个改变的一个主要原因是Es6中Promise的引入,因为这项技术要求对事件循环队列的调度运行能够直接进行精细精确的控制

并行线程 并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行。

  • 一旦有事件需要运行,事件循环就会运行,知道队列清空。事件循环的每一轮称为一个tick。用户交互,IO和定时器会向事件队列中加入事件

第三章

Promise

axios这个库是基于promise的一个库,而promise是es6的一个新的异步请求方式 请求的底层原理还是通过ajax的吧,但是呢?只是回调的方式不太一样了而已。

function foo(x){
  // 开始做点可能耗时的工作
  // 构造一个listener事件通知处理对象返回
  return listener
}
var evt = foo(42)
evt.on("completion",function(){
  // 可以下一步
})
evt.on("failure",function(err){
  // 啊,foo出错了
})

事件订阅对象