ES6/New JS:基础(1)

环境配置

几乎任何文本编辑器都可以编辑JS代码。至于IDE看你自己的喜好。对于初学者,功能越简单越好,支持语法高亮和单词级补全就可以了。

个人推荐:

不管使用哪个编辑器,字符编码必须使用UTF-8

对于Windows记事本、Mac的TextEdit:保存时选择编码:UTF-8。其他IDE大多默认为UTF-8,一般在首选项中可以检查或修改。

想要运行JS,你还需要一个运行环境。如果你想在前端、Web发展,建议用最新版的Chrome;如果你想在后端、全栈发展,建议用最新版的node.js

在浏览器环境下,你需要了解HTML的基本结构,知道用<script>标签嵌入脚本的方法。在node.js环境下,你需要掌握终端(Shell)的使用,能够通过命令行调用node.js。

下文以node.js/Linux为基础。

Hello World!

'use strict'
console.log('Hello World!')
  1. 把上面两行内容保存到文件。比如hello-world.js
  2. 打开终端窗口,切换到文件所在目录。比如cd ~/learn-es6/
  3. 执行:node hello-world

你应该看到终端窗口输出:Hello World!

解释

JS程序不像C++/Java一样需要main函数标识入口点。执行时,JS解释器会执行文件中的每一条指令,如果完成后,事件循环没有待发生事件,程序终止;否则,等待事件发生。

'use strict'是一个特殊的指令。当它是JS源码的第一个表达式时,解释器会进入严格模式(Strict Mode)。严格模式下,一些”不好的“特性被禁止,语法分析会更严格。这个功能在ES5被引入,方便向新版JS过渡。要使用ES6或更新的JS版本,必须进入严格模式。下文所有的代码都需要在严格模式下执行。

console是一个全局对象,由JS解释器提供。在浏览器中表示控制台(你可以按F12或Cmd+Opt+I打开开发者工具,切换到Console面板;在node.js中表示标准输出(stdout),也就是终端窗口的输出。

logconsole对象的一个方法(Method,也称作函数,Function),它可以将参数以字符串格式输出。('Hello World!')表示以'Hello World'为参数,调用方法。

在C++中,上面 Hello World 的实现可能是这样:

#include <iostream>
using namespace std;
int main() {
    cout<<"Hello World!"<<endl;
}

基础 / Basis

JS中很多的关键字、控制结构、字面值(Literal Value)写法与C++/Java相同。

你可以在node.js的REPL(Read-Eval-Print-Loop)中玩耍本文的代码(在终端窗口输入node并执行)。在REPL中,每个输入表达式会被求值,并输出结果。

标识符(变量/常量)声明

let   variable = 123
const constant = 'JavaScript'

letconst遵循块作用域(Block Scope),与C++、Java的变量声明相同。

以前(ES5时代),只能用var来声明标识符。它遵循函数作用域(与我们熟悉的语言不同!声明会被移到函数或源码的开头)。ES6引入letconst后,几乎没有什么地方还会使用它。

如果你对var挖出来的坑感兴趣,可以看这篇文章

ES6下,几乎任何时候都应该使用let或const

基础类型(Primitive Types)

你可以用typeof运算符检查类型,像这样:typeof someVariable。结果会是一个表示类型的字符串。

你可以用NumberBooleanString函数将一个表达式转换成对应的基本值(Primitive)。但加上new操作符时,结果是一个对象。两者不等价!这个特性会让新手感到困惑,会导致糟糕的问题。通常,不要显示地进行类型转换,JS解释器会在需要的时候自动转换。

let a = Number(123)      // => 123
let b = new Number(123)  // => 值为123的Number对象
typeof a    // => 'number'
typeof b    // => 'object'
a === b     // => false

你可能会感到困惑,函数、数组并不在是基本类型。事实上,它们都是对象:Object基础上“扩展”出来的对象。这在对象和原型链部分会进一步解释。

JS的弱类型一方面带来了很高的灵活性(你可以写出很漂亮的代码);另一方面,也会成为程序员的噩梦(尤其对新手来说):因为缺少类型检查(也可以说是提示),你可以写出语法正确、但毫无意义的表达式(比如new Date(true)、你也不能限定函数参数的类型,如果你需要实现API,在函数内对参数类型进行检查是必要的

部分JS引擎中typeof null'object',但根据标准应该是null

如果你很喜欢类型系统,可以去看看TypeScript。但这个类型系统不过是骗骗自己(与Java模版的类型类似,仅仅是提示作用),所以我不推荐它。

null、undefined

null表示已定义,但没有值(刻意留空)undefined表示未定义或未初始化的值

声明但没有被赋值的标识符的值为undefined

let a
console.log(a)    // 输出:undefined

数字 – number

数字为双精度浮点数(对应Java/C++中的double),其绝对值小于(2^53)时,可以被精确表示。

你可以用十进制、八进制(0o前缀)、十六进制(0x0X前缀)表示整数值,但不能以0开头(这是ES5中8进制的写法,现在已经不允许使用):

let dec =   11    // => 11
let oct = 0o11    // => 9
let hex = 0x11    // => 17

浮点数只能用十进制表示。小数点前的0可以省略,但一般建议写上。

let num1 = 0.5
let num2 =  .5    // this is ok, => 0.5

表示是十制数时可以用科学技术法的E标注(E notion),用eE后缀表示数量级:

let e_notion_1 = 1e4    // => 10000
let e_notion_2 = 1e-4   // => 0.0001

浮点数标准规定了两个特殊值:NaN、Infinity。前者表示不是数值(Not a Number),后者表示无穷(Infinity),可以在前面加上-+表示符号。NaN与任何值都不相等(即便NaN===NaN);与任何数值进行计算,结果都是NaN。Infinity与任何数值进行运算操作结果仍是Infinity;Infinity与Infinity进行运算,结果是NaN。

NaN + 1        // => NaN
NaN === NaN    // => false
Infinity + 1e10         // => Infinity
Infinity - Infinity     // => NaN
-Infinity + 123456      // => -Infinity

你可以用parseInt(str, radix)函数将一个字符串以给定进制转换为整数。如果不提供进制信息,JS会根据字符串推断:

parseInt(10.1)    // => 10
parseInt(10, 16)  // => 16,先转换为字符串'10',再按照16进制转换
parseInt('0x10')  // => 16,0x为16进制前缀
parseInt('ff apples', 16)    // => 255,转换到第一个不符合进制规则的字符
parseInt('not a number', 10) // => NaN,不是数字

// 以下两个例子说明radix参数的必要性
parseInt('077') // => 63,    未定义行为。0为8进制前缀
parseInt('087') // => 87,未定义行为。0为8进制前缀

你可以用parseFloatNumber函数将一个字符串转换成浮点值:

parseFloat('-1e3')    // => -1000
Number('1e-4')        // => 0.0001

比较浮点数相等的时候,请务必考虑误差。推荐的方式是:

function isNumberEqual(a, b) {
    // Number为全局对象,提供了与数字类型相关的方法和常量
    // Number.EPSILON为能表示的浮点数之间最小的间隔
    return (Math.abs(a - b) < Number.EPSILON)
}

全局对象Math提供了常见的数学计算函数,与C++的cmath头文件类似,比如正弦、乘方。详细说明参见MDN-Math对象

数字支持算数运算(+-*/)和位运算(~&|^<<>>>>>),运算符与Java/C++中的基本相同。进行位运算时,数字会被当作32位有符号整数

实际上,大多数JS引擎会先把整数当作32位整数处理(提高运算速度),直到执行了不可能在32位整数上执行的操作,或者超过了范围。

如果你想了解2^53+0-0Infinity的来历,可以去了解浮点数的原理和实现

完整的parseInt说明:MDN-parseInt

布尔 – boolean

truefalse分别表示真和假,涉及布尔运算时:

!&&||分别表示逻辑非、逻辑与、逻辑或。!&&的结果一定是truefalse||的结果是第一个真值(truthy,被视为true的值)。

||常用来设置函数参数的默认值。在ES6引入参数默认值后,这种用法逐渐减少。

前面说过,用Boolean(expr)可以将表达式转换为布尔值;用new Boolean(expr)会创建一个值为truefalse的Boolean对象,这个对象会被视为true

let a = false, b = null, c = 'truthy', d = true
(a || b || c || d)    // => 选出第一个真值:'truthy'

// 不要这么写:
if ( new Boolean(false) ) { /* 这里的代码 会   执行! */}
if ( Boolean(false) )     { /* 这里的代码 不会 执行! */}

字符串 – string

根据Unicode标准,字符由其码点(Code Point)标识,码点是一个整数,最多32位。码点可以用多个长度确定(比如8位,16位)的码元(Code Unit)表示。

JS中的字符可以用单引号、双引号、反引号表示,前两者语义上没有任何区别,后者表示模版字符串。模版字符串用来嵌入表达式的值。

JS没有字符类型,通常用包含一个字符的字符串表示它。涉及字符串的方法不区分字符与字符串。

// 以下两种写法没有区别
let str1 = 'quoted'
let str2 = "quoted"

let num = 123
let template = `Number is: ${num}`  // => 'Number is: 123'

JS中,字符串由UTF-16码元(每个码元占16位)组成。大多数字符占用1个码元,少数罕见的字符、符号占用2个码元。字符串的length属性(不是方法!)表示字符串的长度,是UTF-16码元的数量,与字符数量的不同:

'word'.length    // 4码元,4字符
'汉字'.length    // 2码元,2字符
'😂'.length      // 2码元,1字符
'𠮷'.length      // 2码元,1字符

严格来说,操作字符时要判断双码元字符。但通常不需考虑这种情况,UTF-16码元覆盖了世界上大多数语言的常用字符(对中文来说,包括了GBK中的所有字符)。因此,一般认为码点和码元是等价的(即一个码元就是一个字符)。

JS字符串最强大的地方在于内建的正则表达式功能。利用它你可以方便的进行格式验证、提取信息、查找替换。一些方法仅支持正则表达式,它的参数会被隐式转换成正则表达式,新手可能会搞混indexOfsearch方法。两者都返回子串的位置,前者进行字符串查找,后者进行正则表达式匹配。

JS的字符串支持支持大多数Java中的字符串方法。完整的列表可以在MDN-String查阅。所有方法都不会修改被操作字符串的值。

常用的方法如下:

// a为操作的字符串,str为字符串参数,regexp为正则表达式

// 查找、替换:
a.includes(str)    // a是否包含str
a.startsWith(str)  // a是否由str开始
a.endsWith(str)    // a是否由str结束
a.indexOf(str)     // str在a中的位置,如果不在返回-1
a.match(regexp)    // 验证a是否符合regexp的形势
a.search(regexp)   // regexp在a中的位置,如果不在返回-1
a.replace(str, replaceStr)  // 用replaceStr替换str
    // str也可以是正则表达式
    // replaceStr可以是函数: (match)=>result
    //   match为regexp匹配到的字符串,result为替换字串
    //   你可以用这个方法进行复杂的变换

// 子串截取:
//   beg为开始位置,可以为负数
//   end为结束位置,可以为负数
//   start为开始位置,必须 >= 0
//   len为子串常度
//   beg、end为负数时,表示从字符串尾部开始计数
//   以下三个函数的第二个参数可以省略,默认截取到字符串尾部
//   子串不包含end位置的字符,但包括beg位置的字符
//   截取子串,直到满足end、len的条件 或 遇到字符串尾部
//     => 不会像C++一样报越界错误
a.substr(beg, len)
a.slice(beg, end)
a.substring(start, end)

// 变换:
a.trim()    // 去掉两侧的空白字符(正则表达式\s字符组)
            // 包括:空格、tab(\t)、换行符(\n、\r)等
a.toLowerCase()     // 转换为小写
a.toUpperCase()     // 转换为大写
a.split(str|regexp) // 以str或regexp分隔字符串,返回数组

UTF-16的设计是效率与正确性的折衷。目前认为,编码最好的方法是使用UTF-8(每个字符需要1-6个码元编码),但因为它是变长编码(Variable Length Encoding),在处理时需要判定字符边界,或者转换成定长编码(Fixed Length Encoding)。前者对程序猿很不友好,后者占用额外空间。

如果用定长编码表示全部Unicode字符,即UTF-32,每个字符占用4字节空间。单词word需要16字节,这是十分浪费的。而日常使用的字符仅占全部Unicode字符的一部分。所以就有了UTF-16这样包括绝大多数常用字符的设计方式:在绝大多数情况下保持正确,既对程序猿友好,也不需要太多额外空间。

+(连接操作符)可以将字符串与任何对象连接(Concatenation)。任何值与字符串做+操作,结果都是字符串

3 + '2'                  // => '32'
7 + ' apples'            // => '7 apples'
'string ' + undefined    // 'string undefined'

// DOM:没有明显的类型提示,稍不注意就变成字符串连接了
element.getAttribute('data-number') + 1 

对象的字符串表示由toString()方法提供,你可以修改这个方法。(在对象、原型链中会说明)

如果你有兴趣了解UTF-16码元的覆盖情况,可以参考Unicode编码平面。关于Unicode系列编码的比较,可以参考Unicode编码比较

UTF-16作为一种数据处理格式是合适的。但在数据传输、存储(比如数据库、HTTP)的时候必须使用UTF-8