Express查询字符串数组解析为对象的问题

这几天检查日志,每天总会有大量上游的调用请求出现400错误(Expected type array but found type object), 这个API Get请求参数如下:

1
?arr[]=value1&arr[]=value2&arr[]=value3...

进一步分析日志发现,上游的查询参数中数组是有指定索引的,比如

1
?arr[0]=value1&arr[1]=value2&arr[2]=value3...

而且奇怪的是并非所有的请求都有问题,只有当arr[]超过20个才会出现,第一反应是Express的query parser问题,将查询字符串数组解析为对象,

原来Express已经不包含大部分的请求解析中间件了,如json、urlencoded、cookie等中间件都变成可配置的了,只有查询字符串解析中间件还是内置的,其中解析函数默认为qs模块的,可以通过app上的query parse设置。

阅读更多

JavaScript数组随机取一部分不重复元素

从一个 JavaScript 数组当中,随机抽取部分元素,构成新数组,要求这些元素不能重复,即随机获取不重复的数组元素。
这个问题很简单,相信很多人都会在几分钟内给出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function randomMembers1(arr, limit) {
const result = [];

for (let i = 0; i < limit; i++) {
result[i] = arr[Math.floor(Math.random() * arr.length)];
for (let j = 0; j < i; j++) {
if (result[j] === result[i]) {
i--;
break;
}
}
}
return result;
}

randomMembers1([11, 12, 13, 14, 15, 16, 17, 18], 5); //[ 18, 16, 12, 17, 14 ]

解决思路就是从第二次随机抽取的元素开始,将抽取的元素与已抽取元素相比较,如果相同,则重新抽取,并再次执行比较的操作。
但是这种写法存在循环语句和条件语句多层嵌套,复杂度较,执行效率很。随着元素的抽取越多,要比较的次数越来越多,“失败的抽取”概率越来越大。

我们可以优化一下比较的逻辑,比如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function randomMembers2(arr, limit) {
const hash = {};
const result = [];

while (limit > 0) {
const index = Math.floor(Math.random() * arr.length);
if (!hash[index]) {
hash[index] = true;
result.push(arr[index]);
limit--;
}
}

return result;
}

randomMembers2([11, 12, 13, 14, 15, 16, 17, 18], 5); //[ 16, 17, 13, 11, 18 ]

和第一种方法相比,节省了第一种方法中依次比较的步骤,但依旧存在“失败抽取”的现象,而且失败抽取的概率没有发生任何变化。

是否可以把抽取到的元素从数组中删除,从而避免重复抽取呢?可以利用 splice 方法,将抽取到的元素从数组当中删除掉,并把返回值存储(push)到结果数组当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
function randomMembers3(arr, limit) {
const result = [];
let num = arr.length > limit ? limit : arr.length;

while (num > 0) {
const index = Math.floor(Math.random() * arr.length);
result.push(arr.splice(index, 1)[0]);

num--;
}

return result;
}

问题就是这种方法会修改源数组,产生副作用,抽取的元素会从数组中删除。我们可以新建一个数组保存原来数组的下标,从下标数组中进行抽取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function randomMembers4(arr, limit) {
const result = [];
const keyList = [...arr.keys()];
let num = arr.length > limit ? limit : arr.length;

while (num > 0) {
const index = Math.floor(Math.random() * keyList.length);
const key = keyList.splice(index, 1)[0];

result.push(arr[key]);

num--;
}

return result;
}

完整代码

安装指定版本node

mac 环境下,使用 homebrew 安装的 node, 默认是最高版本,如何安装指定版本的 node 呢?

  1. 如果之前使用 brew install node 安装过 node,需要先执行brew unlink node解绑node。

  2. 查找可用的 node 版本 brew search node

  3. 安装你需要的版本, 比如 brew install node@10

  4. 然后 brew link node@10, 这一步可能会报错, 按照提示执行命令就 ok 了, 比如我最后执行的是 brew link --overwrite --force node@10

node -v 不出意外, 就安装好了你想要的 node 版本。

参考文档:

https://www.jianshu.com/p/c5c298486dbd

JavaScript实现四则混合运算

背景

最近在项目中需要自己解析四则混合运算,如果只是简单的加减乘除运算,我相信对大家来说没有任何困难,但是实现带括号的四则运算,还是有一定的难度的。

  • 操作数:小数、整数
  • 运算符:加、减、乘、除
  • 分界符:圆括号 ( ) , 用于指示运算的先后顺序

这里使用逆波兰表达式解决数值运算以及括号带来的优先级提升问题。

逆波兰表达式

  • 中缀表达式(Infix Notation)
    是一个通用的算术或逻辑公式表示方法, 操作符是以中缀形式处于操作数的中间。比如1 + 2 + 3

  • 前缀表达式(Prefix Notation)
    是指将运算符写在前面、操作数写在后面、不包含括号的表达式,而且为了纪念其发明者波兰数学家 Jan Lukasiewicz 所以前缀表达式也叫做波兰表达式。比如- 1 + 2 3

  • 后缀表达式(Postfix Notation)
    与之相反,是指运算符写在操作数后面的不含括号的算术表达式,也叫做逆波兰表达式。比如1 2 3 + -

前后缀表达式的出现是为了方便计算机处理,它的运算符是按照一定的顺序出现,所以求值过程中并不需要使用括号来指定运算顺序,也不需要考虑运算符号(比如加减乘除)的优先级。逆波兰表达式在编译技术中有着普遍的应用。

中缀表达式转换成后缀表达式算法:

  1. 从左至右扫描一中缀表达式。
  2. 若读取的是操作数,则判断该操作数的类型,并将该操作数存入操作数堆栈
  3. 若读取的是运算符:
    1. 该运算符为左括号”(“,则直接存入运算符堆栈。
    2. 该运算符为右括号”)”,则输出运算符堆栈中的运算符到操作数堆栈,直到遇到左括号为止。
    3. 该运算符为非括号运算符:
      1. 若运算符堆栈栈顶的运算符为括号,则直接存入运算符堆栈。
      2. 若比运算符堆栈栈顶的运算符优先级高或相等,则直接存入运算符堆栈。
      3. 若比运算符堆栈栈顶的运算符优先级低或者优先级相等,则输出栈顶运算符到操作数堆栈,直到比运算符堆栈栈顶的运算符优先级低或者为空时才将当前运算符压入运算符堆栈。
    4. 当表达式读取完成后运算符堆栈中尚有运算符时,则依序取出运算符到操作数堆栈,直到运算符堆栈为空。

流程图如下所示:

逆波兰表达式求值算法:

  1. 循环扫描语法单元的项目。
  2. 如果扫描的项目是操作数,则将其压入操作数堆栈,并扫描下一个项目。
  3. 如果扫描的项目是一个二元运算符,则对栈的顶上两个操作数执行该运算。
  4. 如果扫描的项目是一个一元运算符,则对栈的最顶上操作数执行该运算。
  5. 将运算结果重新压入堆栈。
  6. 重复步骤 2-5,堆栈中即为结果值。

算法实现

  • 中缀表达式转换成逆波兰表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const operatorRand = {
'+': 1,
'-': 1,
'*': 2,
'/': 2,
};

/**
* 中缀表达式转换成逆波兰表达式
* @param {string[]} str 中缀表达式
*/
function convert(inputArr) {
if (!Array.isArray(inputArr) || inputArr.length === 0) return [];

const operatorArr = [];
const outputArr = [];

inputArr.forEach(input => {
if (!Number.isNaN(Number(input))) {
// 如果是数字,只接输出
outputArr.push(input);
} else if (input === '(') {
// 如果是左括号,入操作符栈
operatorArr.push(input);
} else if (input === ')') {
// 如果是右括号,循环输出,知道匹配到左括号为止
while (operatorArr.length > 0) {
const operator = operatorArr.pop();
if (operator === '(') break;
outputArr.push(operator);
}
} else {
// 如果是运算符
while (operatorArr.length >= 0) {
const topOperator = operatorArr[operatorArr.length - 1];

// 如果运算符栈为空,或者栈顶运算符是(,或者当前运算符优先级比栈顶运算符优先级高
if (
operatorArr.length === 0 ||
topOperator === '(' ||
operatorRand[input] > operatorRand[topOperator]
) {
operatorArr.push(input);
break;
} else {
outputArr.push(operatorArr.pop());
}
}
}
});

// 输入循环结束,如果运算符栈不为空,循环输出
while (operatorArr.length > 0) {
outputArr.push(operatorArr.pop());
}

return outputArr;
}
  • 逆波兰算法求值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function compute(leftNum, rightNum, operator) {
switch (operator) {
case '+':
return leftNum + rightNum;
case '-':
return leftNum - rightNum;
case '*':
return leftNum * rightNum;
default:
// 除法
return leftNum / rightNum;
}
}

/**
* 计算逆波兰表达式
* @param {string} str 逆波兰表达式
*/
export function count(reversePolishArr) {
if (!Array.isArray(reversePolishArr) || reversePolishArr.length === 0) return 0;

const tmpArr = [];

reversePolishArr.forEach(input => {
if (!Number.isNaN(Number(input))) {
// 数字接直接push
tmpArr.push(Number(input));
} else {
// 运算符
const num1 = tmpArr.pop();
const num2 = tmpArr.pop();

if (isNaN(num1) || isNaN(num2)) {
throw new Error(`无效的表达式:${reversePolishArr.join(',')}`);
}

tmpArr.push(compute(num2, num1, input));
}
});

return Number(tmpArr[0].toFixed(3));
}

完整代码请参考:https://github.com/zubincheung/js-rpn

日志框架winston的使用

日志对于问题定位、调试,系统性能调优至关重要,尤其是系统复杂以及在线运行的情况下。之前的项目日志输出一直用 log4js,输出到一个文件,最近对那一块进行重构。

分别考虑了两款 Node.js 框架,分别是BunyanWinston

  • Winston 是 Node.js 最流行的日志框架之一,设计为一个简单通用的日志库,支持多传输
  • Bunyan 以略微不同的方式处理结构化,机器可读性被重点对待。实际上就是 JSON.stringify 的一个输出。

预期效果

  • 日志分级
  • 根据不同的代码分层来产生不同的 log 输出到不同的文件。
  • 输出到 log 文件同时还可以选择输出到标准输出。
  • 自定义格式化日志输出,输出到标准输出的格式便于读取,输出到 log 文件的格式便于分析。
  • 按天自动切割日志文件。

为何选择 winston

  • github start 数多。
  • 更加灵活,可以灵活的组织 transport,来完成比较复杂的日志输出任务。
  • 日志格式为 json 字符串,方便后期分析,当然可以自定义 format.
  • 支持简单的 log 分析,Profiling。
  • 支持 stream Api。
  • 简单 Log Query Api,当然无法和专业的日志分析工具比。

使用方法

定义标准输出 Transport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ConsoleTransport extends winston.transports.Console {
constructor(options) {
super(options);

this.format = winston.format.combine(
winston.format(info => {
info.hostname = hostname();
info.pid = process.pid;
info.level = info.level.toUpperCase();
return info;
})(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss,SSSS' }),
winston.format.ms(),
winston.format.colorize(),
winston.format.printf(options.formatter),
);
}
}

定义文件输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const winston = require('winston');
require('winston-daily-rotate-file');

class FileTransport extends winston.transports.DailyRotateFile {
constructor(options) {
super(options);

this.datePattern = 'YYYY-MM-DD';
this.zippedArchive = true;
this.maxSize = '100m';
this.maxFiles = '14d';

const defaultFormatter = winston.format.combine(
winston.format(info => {
info.hostname = hostname();
info.pid = process.pid;
info.level = info.level.toUpperCase();
return info;
})(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss,SSSS' }),
winston.format.ms(),
);

if (options.json) {
// 输出json格式
this.format = winston.format.combine(
defaultFormatter,
winston.format.json(options.formatter),
);
} else {
this.format = winston.format.combine(
defaultFormatter,
winston.format.printf(options.formatter),
);
}
}
}

调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const options = {
name: 'app',
module: 'app',
filePath: './logs',
formatter: meta => {
return `${meta.timestamp} ${meta.level} ${meta.hostname} ${meta.pid} (${meta.ms}) [${
meta.module
}] ${meta.message}`;
},
consoleLevel: 'error',
json: false,
};

const consoleTransport = new ConsoleTransport({
level: options.consoleLevel,
name: options.name,
formatter: options.consoleFormatter,
});

const infoTransport = new FileTransport({
level: 'info',
name: options.name,
filename: join(options.filePath, `info/${options.name}-info-%DATE%.log`),
formatter: options.formatter,
json: options.json,
});

const errorTransport = new FileTransport({
level: 'error',
name: options.name,
filename: join(options.filePath, 'error/error-%DATE%.log'),
formatter: options.formatter,
json: options.json,
});

const transports = [consoleTransport, infoTransport, errorTransport];
const logger = winston.createLogger({ transports });

logger.info('log1');
logger.error('error 1');

代码地址为:https://github.com/zubincheung/cw-logger-winston

输出效果

1
2
3
4
5
6
7
8
9
10
11
2018-12-30 10:07:28,4605 INFO zubin-pc.local 59468 (+0ms) [app] app log 1
2018-12-30 10:07:28,4695 ERROR zubin-pc.local 59468 (+1ms) [app] Error: app error 1
Error: app error 1
at Object.it (/Users/zubincheung/ciwong/cw-logger-winston/test/cw-logger.test.js:61:35)
at Object.asyncJestTest (/Users/zubincheung/ciwong/cw-logger-winston/node_modules/jest-jasmine2/build/jasmine_async.js:108:37)
at resolve (/Users/zubincheung/ciwong/cw-logger-winston/node_modules/jest-jasmine2/build/queue_runner.js:56:12)
at new Promise (<anonymous>)
at mapper (/Users/zubincheung/ciwong/cw-logger-winston/node_modules/jest-jasmine2/build/queue_runner.js:43:19)
at promise.then (/Users/zubincheung/ciwong/cw-logger-winston/node_modules/jest-jasmine2/build/queue_runner.js:87:41)
at process.internalTickCallback (internal/process/next_tick.js:77:7)

时区问题小结

修改基于 Alpine 的 Docker 容器的时区

在容器中修改

进入容器

1
# docker exec -it container_name /bin/sh

安装 timezone,列出安装的时区文件,验证是否下载成功。

1
2
# apk add -U tzdata
# ls /usr/share/zoneinfo

拷贝需要的时区文件到 localtime,国内需要的是 Asia/Shanghai:

1
# cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

验证时区

1
2
date
Wed Dec 12 19:09:09 CST 2018

CST 即为 中国标准时间。

移除时区文件:

1
# apk del tzdata

在 Dockerfile 指定时区

1
2
3
4
5
6
7
# Install base packages, set timezone
RUN apk update && apk add curl bash tree tzdata

# cp -r -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

ENV TZ Asia/Shanghai

Node.Js 中 sequelize 时区的配置方法

sequelize 默认情况下,保存日期时会转换成 +00:00 时区

解决方式:

sequelize 时配置时区
timezone: ‘+08:00’

1
2
3
4
5
6
7
8
9
10
11
const sequelize = new Sequelize(config.database, config.username, config.password, {
host: config.host,
port: config.port,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 10000,
},
timezone: '+08:00',
});

参考文档

Setting the timezone
Sequelize.html#instance-constructor-constructor

JavaScript深入之类型转换

JavaScript 是弱类型的语言,它的取值非常灵活。你期望一种类型值的时候,你可以提供任何类型的值,JavaScript 将根据需要自行转换类型,在 JavaScript 中通常将它们统称为 强制类型转换

ECMAScript 规范Type Conversion 章节中定义了转换规则。这里我们着重介绍 **ToPrimitiveToBoolean**、 ToNumberToString

ToPrimitive

在 JavaScript 中,想要将对象转换成原始值,必然会调用 toPrimitive()内部函数,那么它是如何工作的呢?

1
2
ToPrimitive(input [, PreferredType])
input 是输入的值,preferedType 是期望转换的类型,
输入类型 结果
Undefined 返回 input 自身
Null 返回 input 自身
Boolean 返回 input 自身
Number 返回 input 自身
String 返回 input 自身
Symbol 返回 input 自身
Object 返回该对象的默认值。具体过程请(通过内部操作 DefaultValue ,参见 ES5 规范 8.12.8 节)

如果 PreferredType 是 Number,执行顺序如下:

  1. 如果 input 为 primitive,返回。
  2. 否则,input 为 Object。调用 obj.valueOf()。如果结果是 primitive,返回。
  3. 否则,调用 obj.toString(). 如果结果是 primitive,返回。
  4. 如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
  5. 如果 PreferredType 是 String,步骤 2 跟 3 互换,如果 PreferredType 为空,Date 类型默认为 String,其他都是 Number。

ToBoolean

ToBoolean 运算符根据下表将其参数转换为布尔值类型的值:

输入类型 结果
Undefined false
Null false
Boolean 结果等于输入的参数(不转换)。
Number 如果参数是 +0, -0, 或 NaN,结果为 false ;否则结果为 true。
String 如果参数参数是空字符串(其长度为零),结果为 false,否则结果为 true。
Object true
Symbol true

ToNumber

ToNumber 运算符根据下表将其参数转换为数值类型的值:

输入类型 结果
Undefined NaN
Null +0
Boolean 如果参数是 true,结果为 1。如果参数是 false,此结果为 +0。
Number 直接返回。
String 参见下文的文法和注释。
Symbol TypeError
Object 执行下列步骤:
1. 设 primValue 为 ToPrimitive( 输入参数 , hint Number)。
2.返回 ToNumber(primValue)。

ToString

ToString 运算符根据下表将其参数转换为字符串类型的值:

输入类型 结果
Undefined “undefined”
Null “null”
Boolean 如果参数是 true,那么结果为 “true”。
如果参数是 false,那么结果为 “false”。
Number 详见7.1.12.1NumberToString
String 直接返回 。
Symbol TypeError
Object 执行下列步骤:
1、设 primValue 为 ToPrimitive( 输入参数 , hint String)。
2、返回 ToString(设 primValue 为 )。

参考资料

Type Conversion and Testing
Type Conversion

JavaScript深入之引用类型

基本类型

JavaScript 变量可以用来保存两种类型的值:基本类型值和引用类型值。基本类型值指的是 简单的数据段,包括以下 6 种基本数据类型:Undefined、Null、Boolean、Number 、 String 和 Symbol。
基本类型保存在栈中,存储的是具体的值,是轻量级的数据存储方式。

引用类型

引用类型的值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存中的位置, 也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。 为此,引用类型的值是按引用访问的。

Object 类型

Object 是一个基础类型,其他所有类型都从 Object 继承了基本的行为。
创建 Object 实例的方式有两种。第一种是使用 new 操作符后跟 Object 构造函数,另一种方式是使用对象字面量表示法。

1
2
3
4
5
6
7
8
9
10
// new操作符创建对象
let person1 = new Object();
person.name = 'Zubin';
person.age = 18;

// 字面量表示法
let person2 = {
name: 'Zubin',
age: 18,
};

Array 对象

除了 Object 之外,Array 类型恐怕是 ECMAScript 中最常用的类型了。而且,ECMAScript 中 的数组与其他多数语言中的数组有着相当大的区别。

  • ECMAScript 数组的每一项可以保存任何类型的数据。
  • ECMAScript 数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容 纳新增数据。

方法:

  1. Array.from()
    从类数组对象或者可迭代对象中创建一个新的数组实例。

  2. Array.isArray()
    用来判断某个变量是否是一个数组对象。

  3. Array.of()
    根据一组参数来创建新的数组实例,支持任意的参数数量和类型。

数组实例的常用方法:

方法名称 说明
concat 用于连接两个或更多的数组并返回结果,arr1.concat(arr2)
join 把数组的所有元素放入一个字符串,元素通过制定的分隔符进行分离 arr1.join(‘,’)
pop 删除并返回数组中的最后一个元素 arr1.pop()
push 向数组的末尾添加一个或更多元素,并返回新的长度 arr1.push(1)
reverse 颠倒数组中的元素顺序,arr1.reverse()
shift 删除并返回数组中的第一个元素 arr1.shift()
slice 从某个已有的数组返回指定的元素
sort 对数组的元素进行排序 arr1.sort()
splice 删除元素,并向数组中添加新元素
toString 把数组转成字符串 arr1.toString()
toLocaleString 把数组转换为本地字符串 arr1.toLocaleString()
valueOf 返回数组对象的原始值

Date 对象

创建 Date 实例用来处理日期和时间。Date 对象基于 1970 年 1 月 1 日(世界标准时间)起的毫秒数。

1
2
3
4
5
6
7
8
var today = new Date();
var today = new Date(1453094034000); // by timestamp(accurate to the milliseconds)
var birthday = new Date('December 17, 1995 03:24:00');
var birthday = new Date('1995-12-17T03:24:00');
var birthday = new Date(1995, 11, 17);
var birthday = new Date(1995, 11, 17, 3, 24, 0);

var unixTimestamp = Date.now(); // in milliseconds

方法

  1. Date.now()
    返回自 1970-1-1 00:00:00 UTC (世界标准时间)至今所经过的毫秒数。
  2. Date.parse()
    解析一个表示日期的字符串,并返回从 1970-1-1 00:00:00 所经过的毫秒数。
  3. Date.UTC()
    接受和构造函数最长形式的参数相同的参数(从 2 到 7),并返回从 1970-01-01 00:00:00 UTC 开始所经过的毫秒数。

RegExp 类型

类型是 ECMAScript 支持正则表达式的一个接口,提供了最基本的和一些高级的正则表 达式功能。

1
2
3
4
5
6
7
8
9
10
11
var regex1 = /\w+/;
var regex2 = new RegExp('\\w+');

console.log(regex1);
// expected output: /\w+/

console.log(regex2);
// expected output: /\w+/

console.log(regex1 === regex2);
// expected output: false

Function 类型

Function 构造函数 创建一个新的 Function 对象。 在 JavaScript 中, 每个函数实际上都是一个 Function 对象。

1
2
3
4
var sum = new Function('a', 'b', 'return a + b');

console.log(sum(2, 6));
// expected output: 8

参考文档

MDN JavaScript
JavaScript 高级程序设计(第 3 版)

JavaScript深入之原生函数

JavaScript 为基本数据类型值提供了封装对象,称为原生函数,常见的原生函数包括

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

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

1
2
3
4
let a = new String('abc');
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call(a); // "[object String]"

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

内部属性[[Class]]

所有 typeof 返回值为 “object” 的对象(如数组)都包含一个内部属性 [[Class]] (我们可 以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问, 一般通过 Object.prototype.toString(..) 来查看。例如:

1
2
3
4
5
6
7
8
9
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
Object.prototype.toString.call(/regex-literal/i); // "[object RegExp]"

Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"

Object.prototype.toString.call('abc'); // "[object String]"
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"

内部属性和创建该对象的内建原生构造函数相对应,虽然 Null() 和 Undefined() 这样的原生构造函数并不存在,但是内部属性值仍 然是 “Null” 和 “Undefined” 。
其他基本类型值(如字符串、数字和布尔)的情况有所不同,它们的值被各自的封装对象自动包装,所以它们的内部属性值分别为 “String” 、 “Number” 和 “Boolean” 。

封装

原生函数为基本数据类型值提供了该子类型所特有的方法和属性(如:String#trim() 和 Array#concat(..))。

对于简单标量基本类型值,比如 “abc” ,如果要访问它的 length 属性或 String.prototype 方法, JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。

1
2
3
4
5
6
7
8
9
var a = 'abc';

a.length; // 3
a.toUpperCase(); // "ABC"

var b = new Boolean(false);
if (!b) {
console.log('Oops'); // 执行不到这里
}

拆封

如果想要得到封装对象中的基本类型值,可以使用 valueOf() 函数:

1
2
3
4
5
6
7
var a = new String('abc');
var b = new Number(42);
var c = new Boolean(true);

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

参考文档

你不知道的 JavaScript(中卷)

JavaScript深入之数据类型

一、内置类型

最新的 ECMAScript 标准定义了 7 种数据类型:

  • 空值(null)
  • 未定义(undefined)
  • 布尔值(boolean)
  • 数字(number)
  • 字符串(string)
  • 符号(symbol,ES6 中新增)
  • 对象(object)

除对象为“复杂类型”之外,其他统称为“基本类型”。

阅读更多