后台mybatis 查询返回null给前端的时间是null,为什么一转换就有thu jan 01 1970 08:00:00 gmt+0800

标签:至少1个,最多5个
最近使用 sequelize 过程中发现一个“奇怪”的问题,将某个时间插入到表中后,通过 sequelize 查询出来的时间和通过 mysql 命令行工具查询出来的时间不一样。非常困惑,于是研究了下,下面是学习成果。
我们先来介绍一些可能当年在地理课上学习过的基本概念。
说起来,时间真是一个神奇的东西。以前人们通过观察太阳的位置来决定时间(比如:使用日晷),这就使得不同经纬度的地区时间是不一样的。后来人们进一步规定以子午线为中心,向东西两侧延伸,每 15 度划分一个时区,刚好是 24 个时区。然后因为一天有 24 小时,地球自转一圈是 360 度,360 度 / 24 小时 = 15 度/小时,所以每差一个时区,时间就差一个小时。
最开始的标准时间(子午线中心处的时间)是英国伦敦的皇家格林威治天文台的标准时间(因为它刚好在本初子午线经过的地方),这就是我们常说的 GMT(Greenwich Mean Time)。然后其他各个时区根据标准时间确定自己的时间,往东的时区时间晚(表示为 GMT+hh:mm)、往西的时区时间早(表示为 GMT-hh:mm)。比如,中国标准时间是东八区,我们的时间就总是比 GMT 时间晚 8 小时,他们在凌晨 1 点,我们已经是早晨 9 点了。
但是 GMT 其实是根据地球自转、公转计算的(太阳每天经过英国伦敦皇家格林威治天文台的时间为中午 12 点),不是非常准确,于是后面提出了根据原子钟计算的标准时间 UTC(Coordinated Universal Time)。
一般情况下,GMT 和 UTC 可以互换,但是实际上,GMT 是一个时区,而 UTC 是一个时间标准。
可以在这里看到所有的时区:
所以,当我们“展示”某个时间时,明确时区就变得非常重要了。不然你只说现在是
19:30:00,然后不告诉我时区,我其实是没法准确知道时间的(当然,我可以认为这个时间是我所在时区的当地时间)。如果你说现在是
19:30:00 GMT+0800,那我就知道这个时间是东八区的时间了。如果我在东八区,那时间就是 19:30,如果我在 GMT 时区,那时间就是 11:30(减掉 8 小时)。
JavaScript 中的“时间”
我们现在来介绍下 JavaScript 中的“时间”,包括:Date、Date.parse、Date.UTC、Date.now。
注:下面的代码示例可以在 node shell 里面运行,如果你运行的时候结果和下面的不一致,那可能咱们不在一个时区:)
Date 构造器
构造时间的方法有下面几种:
new Date();
// 当前时间
new Date(value);
00:00:00 UTC 经过的毫秒数
new Date(dateString); // 时间字符串
new Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]);
需要注意的是:构造出的日期用来显示时,会被转换为本地时间(调用 toString 方法):
& new Date()
Mon Jan 11 :18 GMT+0800 (CST)
打印出我写这篇文章时的本地时间。后面的 GMT+0800 表示是“东八区”,CST 表示是“中国标准时间(China Standard Time)”。
有一个很“诡异”的地方是如果我们直接使用 Date,而不是 new Date,得到的将会是字符串,而不是 Date 类型的对象:
& typeof Date()
& typeof new Date()
时间字符串
我们先说最复杂的时间字符串形式。它实际上支持两种格式:一种是 RFC-2822 的标准;另一种是 ISO 8601 的标准。我们主要介绍后一种。
ISO 8601的标准格式是:YYYY-MM-DDTHH:mm:ss.sssZ,分别表示:
YYYY:年份,0000 ~ 9999
MM:月份,01 ~ 12
DD:日,01 ~ 31
T:分隔日期和时间
HH:小时,00 ~ 24
mm:分钟,00 ~ 59
ss:秒,00 ~ 59
.sss:毫秒
Z:时区,可以是:Z(UFC)、+HH:mm、-HH:mm
这里我们主要来说下 T、以及 Z。
T 也可以用空格表示,但是这两种表示有点不一样,T 其实表示 UTC,而空格会被认为是本地时区(前提是不通过 Z 指定时区)。比如下面的例子:
& new Date(' 00:00:00')
Thu Jan 01 :00 GMT+0800 (CST)
& new Date('T00:00:00')
Thu Jan 01 :00 GMT+0800 (CST)
示例 1 的空格表示法被当做了本地时区,所以显示的时间和传入的时间一致。
示例 2 的 T 被当做 UTC 时间,所以显示的时间会加上本地时区(东八区)的 8 小时偏移。实际上,T00:00:00 等价于
00:00:00Z。
Z 用来表示传入时间的时区(zone),不指定并且没有使用 T 分隔而是使用空格分隔时,就按本地时区处理,比如下面的例子:
& new Date('T00:00:00+08:00')
Thu Jan 01 :00 GMT+0800 (CST)
& new Date(' 00:00:00')
Thu Jan 01 :00 GMT+0800 (CST)
& new Date('T00:00:00+00:00')
Thu Jan 01 :00 GMT+0800 (CST)
示例 1 是东八区时间,显示的时间和传入的时间一致(因为我本地时区是东八区)。
示例 2 和示例 1 结果一样,不指定时区就是本地时区。
示例 3 指定时区为 GMT 时区(偏移为 0),显示的时间会加上本地时区的偏移(8 小时)。
RFC-2822 的标准格式大概是这样:Wed Mar 25 :24 GMT+0100。其实就是上面显示时间时使用的形式:
& new Date('Thu Jan 01 :00 GMT+0800 (CST)')
Thu Jan 01 :00 GMT+0800 (CST)
除了能表示基本信息,还可以表示星期,但是一点也不容易读,不建议使用。完整的规范可以在这里查看:
Date 构造器还可以接受整数,表示想要构造的时间自 UTC 时间
00:00:00 经过的毫秒数。比如下面的代码:
& new Date(1000 * 1)
Thu Jan 01 :01 GMT+0800 (CST)
传人 1 秒,等价于: 00:00:01Z,显示的时间加上了本地时区的偏移(8 小时)。
最后,Date 构造器还支持传递多个参数,这种方法就没办法指定时区了,都当做本地时间处理。比如下面的代码:
& new Date(, 0, 0, 0)
Thu Jan 01 :00 GMT+0800 (CST)
显示时间和传入时间一致,均是本地时间。注意:月份是从 0 开始的。
Date.parse
Date.parse 接受一个时间字符串,如果字符串能正确解析就返回自 UTC 时间
00:00:00 经过的毫秒数,否则返回 NaN:
& Date.parse(' 00:00:00')
& new Date(Date.parse(' 00:00:00'))
Thu Jan 01 :00 GMT+0800 (CST)
& Date.parse('T00:00:00')
& new Date(Date.parse('T00:00:00'))
Thu Jan 01 :00 GMT+0800 (CST)
示例 1,- 换算后刚好是 8 小时表示的毫秒数, / (1000 * 60 * 60),我们传入的是本地时区时间,等于 UTC 时间的
16:00:00,和 UTC 时间
00:00:00 相差刚好 -8 小时。
示例 2,将 parse 后的毫秒数传递给构造器,最后显示的时间加上了本地时区的偏移(8 小时),所以结果刚好是
00:00:00。
示例 3,传入的是 UTC 时区时间,所以结果为 0。
示例 4,将 parse 后的毫秒数传递给构造器,最后显示的时间加上了本地时区的偏移(8 小时),所以结果刚好是
08:00:00。
Date.UTC 接受的参数和 Date 构造器多参数形式一样,然后返回时间自 UTC
00:00:00 经过的毫秒数:
& Date.UTC(,0,0,0)
& Date.parse('T00:00:00')
& Date.parse(' 00:00:00Z')
可以看出,Date.UTC 进行的是一种“绝对运算”,传入的时间就是 UTC 时间,不会转换为当地时间。
Date.now 返回当前时间距 UTC 时间
00:00:00 经过的毫秒数:
& Date.now()
& new Date(Date.now())
Mon Jan 11 :55 GMT+0800 (CST)
& new Date()
Mon Jan 11 :00 GMT+0800 (CST)
MySQL 中的“时间”
MySQL 中和时间相关的数据类型主要包括:YEAR、TIME、DATE、DATETIME、TIMESTAMP。
DATE、YEAR、TIME 比较简单,大概总结如下:
1901 ~ 2155
-838:59:59 ~ 838:59:59
注:TIME 的小时范围可以这么大(超过 24 小时),是因为它还可以用来表示两个时间点之差。
DATEIME vs TIMESTAMP
我们主要来说明下 DATETIME 和 TIMESTAMP,可以做下面的总结:
受 time_zone 设置影响
00:00:00 ~
00:00:00 ~
第一个区别是占用字节不同,导致能表示的时间范围也不一样。
第二个区别是 DATETIME 是“常量”,保存时就是保存时的值,检索时是一样的值,不会改变;而 TIMESTAMP 则是“变量”,保存时数据库服务器将其从time_zone 时区转换为 UTC 时间后保存,检索时将其转换从 UTC 时间转换为 time_zone 时区时间后返回。
比如,我们有下面这样一张表:
CREATE TABLE `tests` (
`id` INTEGER NOT NULL auto_increment ,
`datetime` DATETIME,
`timestamp` TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
连接到数据库服务器后,可以执行 SHOW VARIABLES LIKE '%time_zone%' 查看当前时区设置。类似下面这样的结果:
Variable_name
system_time_zone
说明我目前时区是 CST(China
Standard Time),也就是东八区。
我们尝试插入下面的数据:
INSERT INTO `tests` (`id`, `datetime`, `timestamp`) VALUES (DEFAULT, ' 00:00:00', ' 00:00:00');
会发现有一个报错:Error Code: 1292. Incorrect datetime value: ' 00:00:00' for column 'timestamp'。给 timestamp 这一列提供的值不对,因为我们尝试插入
00:00:00 时,数据库服务器会根据 time_zone 的设置将其转换为 UTC 时间,也就是
16:00:00,而这个值明显超过了 TIMESTAMP 类型的范围。
我们换个大一点的值:
INSERT INTO `tests` (`id`, `datetime`, `timestamp`) VALUES (DEFAULT, ' 00:00:00', ' 00:00:00');
这次就成功插入了。
再次检索时结果也是正确的(数据库服务器将值从 UTC 时间转换为 time_zone 设置的时区时间):
SELECT * FROM sample.
如果我们先将 time_zone 设置为一个不同的值后再进行检索就会发现不同的结果:
SET time_zone = '+00:00';
SELECT * FROM sample.
可以看到 datetime 列值没有受 time_zone 设置的影响,而 timestamp 列值却改变了。数据库服务器将其从 UTC 时区转换为 time_zone 时区的时间(首先
00:00:00 在上面进行插入时根据 time_zone 被转换为了
16:00:00,此次检索时 time_zone 被设置为 +00:00,转换回来刚好就是
16:00:00)。
那这两种类型怎么选择呢?建议优先使用 DATETIME,表示范围大、不容易受服务器的设置影响。
在 JavaScript 和 MySQL 间转换
分别说明了 JavaScript 和 MySQL 中的“时间”后,我们来聊聊 ORM 框架一般都是怎么样在两者间进行正确、合适的转换来避免混乱的。下面的说明将基于 sequelize 框架来解释,主要是一种思路,其他的框架可以阅读框架提供的文档或是源码。
sequelize 实际上有一个 timezone 的配置,默认是 +00:00()。这个 timezone 有下面的用途:
建立数据库连接时,执行 SET time_zone = opts.timezone
MySQL 的时间类型和 JavaScript 的时间类型的互相转换
第一个用途很简单,体现在源码里就是执行一个 SQL 语句:
connection.query("SET time_zone = '" + self.sequelize.options.timezone + "'"); /* jshint ignore: line */
第二个用途主要体现在两个地方:1)在 JavaScript 中调用 ORM 方法进行插入、更新时,需要将 Date 类型转为正确的 SQL 语句;2)从 MySQL 服务器查询数据时,需要将数据库查询到的值转换为 JavaScript 中的 Date 类型。下面我们分别来看一看。
JavaScript -& MySQL
这个转换的核心代码如下:
SqlString.dateToString = function(date, timeZone, dialect) {
if (moment.tz.zone(timeZone)) {
date = moment(date).tz(timeZone);
date = moment(date).utcOffset(timeZone);
if (dialect === 'mysql' || dialect === 'mariadb') {
return date.format('YYYY-MM-DD HH:mm:ss');
// ZZ here means current timezone, _not_ UTC
return date.format('YYYY-MM-DD HH:mm:ss.SSS Z');
代码逻辑如下:
检查 timeZone 是否存在,如果存在(存在指的是类似 America/New_York 这样的表示法),调用 tz 设置 date 的时区。
如果不存在(类似 +00:00、-07:00 这样的表示法),调用 utcOffset 设置 date 的相对 UTC 的时区偏移。
最后使用上面设置的时区偏移将其 format 成 MySQL 需要的 YYYY-MM-DD HH:mm:ss 格式。
举两个例子。
如果 timeZone 等于 +00:00,date 等于 new Date(' 09:46:00'),到 UTC 的偏移等于 (timeZone - 本地时区) + timeZone:(00:00 - 08:00) + 00:00 = -08:00,即
09:46:00-08:00,于是 format 后的结果是
01:46:00。
如果 timeZone 等于 +08:00,date 等于 new Date(' 09:46:00'),到 UTC 的偏移等于 (timeZone - 本地时区) + timeZone:(08:00 - 08:00) + 08:00 = 08:00,即
09:46:00+08:00。于是 format 后的结果是
09:46:00。
如果 timeZone 等于 Asia/Shanghai,结果也会是
09:46:00,和 +08:00 等价。
sequelize 的 timezone 默认是 +00:00,所以,我们在 JavaScript 中的时间最后应用到数据库中都会被转换成 UTC 的时间(比实际的时间早 8 小时)。
MySQL -& JavaScript
这个转换过程实际上是更底层的 node-mysql 库来实现的。核心代码如下:
switch (field.type) {
case Types.TIMESTAMP:
case Types.DATE:
case Types.DATETIME:
case Types.NEWDATE:
var dateString = parser.parseLengthCodedString();
if (dateStrings) {
return dateS
if (dateString === null) {
var originalString = dateS
if (field.type === Types.DATE) {
dateString += ' 00:00:00';
if (timeZone !== 'local') {
dateString += ' ' + timeZ
dt = new Date(dateString);
if (isNaN(dt.getTime())) {
return originalS
// 更多代码...
处理过程大概是这样:
用 parser 将服务器返回的二进制数据解析为时间字符串
如果配置了强制返回字符串 dateStrings 而不是转换回 Date 类型,直接返回 dateString
如果字段类型是 DATE,时间字符串的时间部分统一为 00:00:00
如果配置的 timeZone 不是 local(本地时区),时间字符串加上时区信息
将时间字符串传给 Date 构造器,如果构造出的时间不合法,返回原始时间字符串,否则返回时间对象
默认情况下,sequelize 在进行连接时传递给 node-mysql 的 timeZone 是 +00:00,所以,第 4 步的时间字符串会是类似这样的值
01:46:00+00:00,而这个值传递给 Date 构造器,在显示时转换回本地时区时间,就变成了
09:46:00(比数据库中的时间晚 8 小时)。
在使用 sequelize 定义模型时,其实是没有 TIMESTAMP 类型的,sequelize 只提供了一个 Sequelize.DATE 类型,生成建表语句时被转换为 DATETIME。
如果是在旧表上定义模型,而这张旧表刚好有 TIMESTAMP 类型的列,对 TIMESTAMP 类型的列定义模型时还是可以使用 Sequelize.DATE,对操作没有任何影响。但是 TIMESTAMP 是受 time_zone 设置影响的,这会引起一些困惑。下面我们来看一个例子。
sequelize 默认将 time_zone 设置为 +00:00,当我们执行下面代码时:
Test.create({
'datetime': new Date(' 20:07:00'),
'timestamp': new Date(' 20:07:00')
会进行上面提到的 JavaScript 时间到 MySQL 时间字符串的转换,生成的 SQL 其实是(时间被转换为了 UTC 时间,比本地时间早了 8 小时):
INSERT INTO `tests` (`id`,`datetime`,`timestamp`) VALUES (DEFAULT,' 12:07:00',' 12:07:00');
当我们执行 Test.findAll() 来查询数据时,会进行上面提到的 MySQL 时间到 JavaScript 时间的转换,其实就是返回这样的结果(显示时时间从 UTC 时间转换回了本地时间):
& new Date(' 12:07:00+00:00')
Sun Jan 10 :00 GMT+0800 (CST)
和我们插入时的时间是一致的。
如果我们通过 MySQL 命令行来查询数据时,发现其实是这样的结果:
这很好理解,因为我们数据库服务器的 time_zone 默认是东八区,TIMESTAMP 是受时区影响的,查询时被数据库服务器从 UTC 时间转换回了 time_zone 时区时间;DATETIME 不受影响,还是 UTC 时间。
如果我们先执行 SET time_zone = '+00:00',再进行查询,那结果就都会是 UTC 时间了。所以,不要以为数据出错了哦。
总结下就是,sequelize 会将本地时间转换为 UTC 时间后入库,查询时再将 UTC 时间转换为本地时间。这能达到最好的兼容性,存储总是使用 UTC 时间,展示时应用端自己转换为本地时区时间后显示。当然这个的前提是数据类型选用 DATETIME。
兼容老数据
这里要说的最后一个问题是基于旧表定义 sequelize 模型,并且表中时间值插入时没有转换为 UTC 时间(全部是东八区时间),而且 DATETIME 和 TIMESTAMP 混用,该怎么办?
在默认配置下,情况如下:
查询 DATETIME 类型数据时,时间总是会晚 8 小时。比如,数据库中某条老数据的时间是
01:00:00(已经是本地时间了,因为没转换),查询时被 sequelize 转换为 new Date(' 01:00:00+00:00'),显示时转换为本地时间
09:00:00,结果显然不对。
查询 TIMESTAMP 类型数据时,时间是正确的。这是因为 TIMESTAMP 受 time_zone 影响,sequelize 默认将其设置为 +00:00,查询时数据库服务器先将时间转换到 time_zone 设置的时区时间,由于没有时区偏移,刚好查出来的就是数据库中的值。比如: 00:00:00(注意这个值是 UTC 时间),sequelize 将其转换为 new Date(' 00:00:00+00:00'),显示时转换为本地时间
08:00:00,刚好“侥幸”正确。
新插入的数据 sequelize 会进行上一部分说的双向转换来保证结果的正确。
维持默认配置显然导致查询 DATETIME 不准确,解决方法就是将 sequelize 的 timezone 配置为 +08:00。这样一来,情况变成下面这样:
查询 DATETIME 类型数据时,时间
01:00:00 被转换为 new Date(' 01:00:00+08:00'),显示时转换为本地时间
01:00:00,结果正确。
查询 TIMESTAMP 类型数据时,由于 time_zone 被设置为了 +08:00,数据库服务器先将库中 UTC 时间
00:00:00 转换到 time_zone 时区时间(加上 8 小时偏移)为
08:00:00,sequelize 将其转换为 new Date(' 08:00:00+08:00'),显示时转换为本地时间
08:00:00,结果正确。
插入、更新数据时,所有 JavaScript 时间会转换为东八区时间入库。
这样带来的问题是,所有入库时间都是东八区时间,如果有其他应用的时区不是东八区,那就需要自己基于东八区时间计算偏移并转换时间后显示了。
一不小心写的有点长了,下面列出参考资料供大家进一步学习:
《MySQL 技术内幕》
11 收藏&&|&&47
你可能感兴趣的文章
3 收藏,654
本作品采用 署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可
其实你弄错了,ISO8601只定义了“T”作为分隔符,如果不指明时区的话,应该指的是本地时区。这可能是Date类的Bug,反正这个接口早就废弃了。另外一种格式用空格其实根本不是ISO8601,因为中国的日期表示正好就是YYYY-MM-DD啊,这个效果就和美国的01/11/:18格式对应
分享到微博?
你好!看起来你挺喜欢这个内容,但是你还没有注册帐号。 当你创建了帐号,我们能准确地追踪你关注的问题,在有新答案或内容的时候收到网页和邮件通知。还能直接向作者咨询更多细节。如果上面的内容有帮助,记得点赞 (????)? 表示感谢。
明天提醒我
我要该,理由是:2439人阅读
前端开发(24)
javascipt(36)
Date构造函数New Date() | Date()没有参数默认就是系统当前时区的时间,结果:Sat Apr11 :09 GMT+0800 (China Standard Time)生成代表当前系统的时间,不用new也可以生成,Date本身就是个方法。New Date(milliseconds)这个毫秒参数是从 00:00:00到现在所经过的时间。New Date(datestring)ECMA没有规定,所以只列出目前支持的格式。ISO格式时间ISO格式的时间最显著的特点是年月日之间使用连字符(-)分开,日期和时间可能有一个字母(T),时间后面可能有一个字母Z或时差。T没有意义,只是表示日期和时间是在这里分割开的;Z表示这个时间是UTC时间。
Thu Jan 01 :00 GMT+0800 (China Standard
Firefox37不支持
Thu Jan 01 :00 GMT+0800 (China Standard
Firefox37不支持
:00:00+04:00
Thu Jan 01 2015 04:00:00 GMT+0800 (China Standard Time)
Firefox37不支持
00:00:00+04:00
Thu Jan 01 2015 04:00:00 GMT+0800 (China Standard Time)
Firefox37不支持
00:00:00Z+08:00
Thu Jan 01 :00 GMT+0800 (China Standard
Firefox37不支持
Thu Jan 01 :00 GMT+0800 (China Standard
Thu Jan 01 :00 GMT+0800 (China Standard
00:00:00+08:00
Thu Jan 01 :00 GMT+0800 (China Standard
Thu Jan 01 2015 08:00:00 GMT+0800 (China Standard Time)
00:00:00Z+08:00
Thu Jan 01 :00 GMT+0800 (China Standard
ISO标准格式
Thu Jan 01 2015 08:00:00 GMT+0800 (China Standard Time)
Thu Jan 01 2015 08:00:00 GMT+0800 (China Standard Time)
Thu Jan 01 2015 08:00:00 GMT+0800 (China Standard Time)
T00:00:00Z
Thu Jan 01 2015 08:00:00 GMT+0800 (China Standard Time)
T00:00:00+08:00
Thu Jan 01 :00 GMT+0800 (China Standard
- 00:00:00
不支持公元前,返回的是
Thu Jan 01 :00 GMT+0800
(China Standard Time) 规律总结:带时区的日期时间字符串表示的是指定时区的时间。不带时区的日期时间字符串,则分两种情况:类ISO结构的日期时间字符串表示的是系统当前时区的时间,而ISO标准结构的日期时间字符串表示的是格林尼治子午线的当前时间。Z或z的作用不仅仅是表示是UTC时间,Z同时说明这个日期时间字符串表示的是0时差偏移的时间,视同于格林尼治的子午线时间。但Z后可以继续加时差,偏移从0开始计算。因为Date是在客户端生成的,因此实际的Date已经换算成了本地系统的时间,使用的时候需要注意,日期时间字符串变成Date对象之后,时间和日期的值可能和字符串中的值是有时差的。其它支持的格式
Thu Jan 02 :00 GMT+0800 (China Standard
Thu Jan 02 :00 GMT+0800 (China Standard
Thu Jan 02 :00 GMT+0800 (China Standard
Thu Jan 02 :00 GMT+0800 (China Standard
01/02/2015
Thu Jan 02 :00 GMT+0800 (China Standard
Thu Jan 02 :00 GMT+0800 (China Standard
Thu Jan 02 :00 GMT+0800 (China Standard
01/02 2015
Thu Jan 02 :00 GMT+0800 (China Standard
Thu Jan 02 :00 GMT+0800 (China Standard
IE5-10不支持这个日期格式
Thu Jan 02 :00 GMT+0800 (China Standard
IE5-10不支持这个日期格式
Thu Jan 02 :00 GMT+0800 (China Standard
IE5-10不支持这个日期格式
Thu Jan 02 :00 GMT+0800 (China Standard
IE5-10不支持这个日期格式
May 1,2015
Fri May 01 :00 GMT+0800 (China Standard
May 1 2015
Fri May 01 :00 GMT+0800 (China Standard
May,1,2015
Fri May 01 :00 GMT+0800 (China Standard
May,1 2015
Fri May 01 :00 GMT+0800 (China Standard
各地日期表示格式
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:800506次
积分:7976
积分:7976
排名:第2536名
原创:152篇
转载:17篇
译文:12篇
评论:91条
(3)(1)(2)(1)(4)(2)(2)(3)(1)(1)(2)(3)(8)(2)(1)(3)(2)(1)(3)(2)(1)(4)(1)(1)(1)(3)(4)(2)(2)(4)(1)(4)(1)(2)(1)(1)(2)(1)(1)(2)(2)(1)(4)(2)(2)(8)(4)(5)(17)(5)(8)(2)(1)(5)(7)(17)(3)(1)(1)本帖子已过去太久远了,不再提供回复功能。JavaScript标准教程之标准库 - 简书
JavaScript标准教程之标准库
字数 35640
第3章 标准库
Object对象
JavaScript原生提供一个Object对象(注意起首的O是大写),所有其他对象都继承自这个对象。Object本身也是一个构造函数,可以直接通过它来生成新对象。
var o = new Object();
Object作为构造函数使用时,可以接受一个参数。如果该参数是一个对象,则直接返回这个对象;如果是一个原始类型的值,则返回该值对应的包装对象。
var o1 = {a: 1};
var o2 = new Object(o1);
o1 === o2 // true
new Object(123) instanceof Number
注意,通过new Object()的写法生成新对象,与字面量的写法o = {}是等价的。
与其他构造函数一样,如果要在Object对象上面部署一个方法,有两种做法。
(1)部署在Object对象本身
比如,在Object对象上面定义一个print方法,显示其他对象的内容。
Object.print = function(o){ console.log(o) };
var o = new Object();
Object.print(o)
(2)部署在Object.prototype对象
所有构造函数都有一个prototype属性,指向一个原型对象。凡是定义在Object.prototype对象上面的属性和方法,将被所有实例对象共享。(关于prototype属性的详细解释,参见《面向对象编程》一章。)
Object.prototype.print = function(){ console.log(this)};
var o = new Object();
o.print() // Object
上面代码在Object.prototype定义了一个print方法,然后生成一个Object的实例o。o直接继承了Object.prototype的属性和方法,可以在自身调用它们,也就是说,o对象的print方法实质上是调用Object.prototype.print方法。。
可以看到,尽管上面两种写法的print方法功能相同,但是用法是不一样的,因此必须区分“构造函数的方法”和“实例对象的方法”。
Object对象的方法
Object本身当作工具方法使用时,可以将任意值转为对象。其中,原始类型的值转为对应的包装对象(参见《原始类型的包装对象》一节)。
Object() // 返回一个空对象
Object(undefined) // 返回一个空对象
Object(null) // 返回一个空对象
Object(1) // 等同于 new Number(1)
Object('foo') // 等同于 new String('foo')
Object(true) // 等同于 new Boolean(true)
Object([]) // 返回原数组
Object({}) // 返回原对象
Object(function(){}) // 返回原函数
上面代码表示Object函数将各种值,转为对应的对象。
如果Object函数的参数是一个对象,它总是返回原对象。利用这一点,可以写一个判断变量是否为对象的函数。
function isObject(value) {
return value === Object(value);
Object.keys(),Object.getOwnPropertyNames()
Object.keys方法和Object.getOwnPropertyNames方法很相似,一般用来遍历对象的属性。它们的参数都是一个对象,都返回一个数组,该数组的成员都是对象自身的(而不是继承的)所有属性名。它们的区别在于,Object.keys方法只返回可枚举的属性(关于可枚举性的详细解释见后文),Object.getOwnPropertyNames方法还返回不可枚举的属性名。
Object.keys(o)
// ["p1", "p2"]
Object.getOwnPropertyNames(o)
// ["p1", "p2"]
上面的代码表示,对于一般的对象来说,这两个方法返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。
var a = ["Hello", "World"];
Object.keys(a)
// ["0", "1"]
Object.getOwnPropertyNames(a)
// ["0", "1", "length"]
上面代码中,数组的length属性是不可枚举的属性,所以只出现在Object.getOwnPropertyNames方法的返回结果中。
由于JavaScript没有提供计算对象属性个数的方法,所以可以用这两个方法代替。
Object.keys(o).length
Object.getOwnPropertyNames(o).length
一般情况下,几乎总是使用Object.keys方法,遍历数组的属性。
Object.observe()
Object.observe方法用于观察对象属性的变化。
var o = {};
Object.observe(o, function(changes) {
changes.forEach(function(change) {
console.log(change.type, change.name, change.oldValue);
o.foo = 1; // add, 'foo', undefined
o.foo = 2; // update, 'foo', 1
delete o. // delete, 'foo', 2
上面代码表示,通过Object.observe函数,对o对象指定回调函数。一旦o对象的属性出现任何变化,就会调用回调函数,回调函数通过一个参数对象读取o的属性变化的信息。
该方法非常新,只有Chrome浏览器的最新版本才部署。
除了上面提到的方法,Object还有不少其他方法,将在后文逐一详细介绍。
(1)对象属性模型的相关方法
Object.getOwnPropertyDescriptor():获取某个属性的attributes对象。
Object.defineProperty():通过attributes对象,定义某个属性。
Object.defineProperties():通过attributes对象,定义多个属性。
Object.getOwnPropertyNames():返回直接定义在某个对象上面的全部属性的名称。
(2)控制对象状态的方法
Object.preventExtensions():防止对象扩展。
Object.isExtensible():判断对象是否可扩展。
Object.seal():禁止对象配置。
Object.isSealed():判断一个对象是否可配置。
Object.freeze():冻结一个对象。
Object.isFrozen():判断一个对象是否被冻结。
(3)原型链相关方法
Object.create():生成一个新对象,并该对象的原型。
Object.getPrototypeOf():获取对象的Prototype对象。
Object实例对象的方法
除了Object对象本身的方法,还有不少方法是部署在Object.prototype对象上的,所有Object的实例对象都继承了这些方法。
Object实例对象的方法,主要有以下六个。
valueOf():返回当前对象对应的值。
toString():返回当前对象对应的字符串形式。
toLocaleString():返回当前对象对应的本地字符串形式。
hasOwnProperty():判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。
isPrototypeOf():判断当前对象是否为另一个对象的原型。
propertyIsEnumerable():判断某个属性是否可枚举。
本节介绍前两个方法,其他方法将在后文相关章节介绍。
Object.prototype.valueOf()
valueOf方法的作用是返回一个对象的“值”,默认情况下返回对象本身。
var o = new Object();
o.valueOf() === o // true
上面代码比较o.valueOf()与o本身,两者是一样的。
valueOf方法的主要用途是,JavaScript自动类型转换时会默认调用这个方法(详见《数据类型转换》一节)。
var o = new Object();
1 + o // "1[object Object]"
上面代码将对象o与数字1相加,这时JavaScript就会默认调用valueOf()方法。所以,如果自定义valueOf方法,就可以得到想要的结果。
var o = new Object();
o.valueOf = function (){
1 + o // 3
上面代码自定义了o对象的valueOf方法,于是1 + o就得到了3。这种方法就相当于用o.valueOf覆盖Object.prototype.valueOf。
Object.prototype.toString()
toString方法的作用是返回一个对象的字符串形式,默认情况下返回类型字符串。
var o1 = new Object();
o1.toString() // "[object Object]"
var o2 = {a:1};
o2.toString() // "[object Object]"
上面代码表示,对于一个对象调用toString方法,会返回字符串[object Object],该字符串说明对象的类型。
字符串[object Object]本身没有太大的用处,但是通过自定义toString方法,可以让对象在自动类型转换时,得到想要的字符串形式。
var o = new Object();
o.toString = function () {
return 'hello';
o + ' ' + 'world' // "hello world"
上面代码表示,当对象用于字符串加法时,会自动调用toString方法。由于自定义了toString方法,所以返回字符串hello world。
数组、字符串、函数、Date对象都分别部署了自己版本的toString方法,覆盖了Object.prototype.toString方法。
[1, 2, 3].toString() // "1,2,3"
'123'.toString() // "123"
(function () {
return 123;
}).toString()
// "function () {
return 123;
(new Date()).toString()
// "Tue May 10 :31 GMT+0800 (CST)"
toString()的应用:判断数据类型
Object.prototype.toString方法返回对象的类型字符串,因此可以用来判断一个值的类型。
var o = {};
o.toString() // "[object Object]"
上面代码调用空对象的toString方法,结果返回一个字符串object Object,其中第二个Object表示该值的构造函数。这是一个十分有用的判断数据类型的方法。
实例对象可能会自定义toString方法,覆盖掉Object.prototype.toString方法。通过函数的call方法,可以在任意值上调用Object.prototype.toString方法,帮助我们判断这个值的类型。
Object.prototype.toString.call(value)
不同数据类型的Object.prototype.toString方法返回值如下。
数值:返回[object Number]。
字符串:返回[object String]。
布尔值:返回[object Boolean]。
undefined:返回[object Undefined]。
null:返回[object Null]。
数组:返回[object Array]。
arguments对象:返回[object Arguments]。
函数:返回[object Function]。
Error对象:返回[object Error]。
Date对象:返回[object Date]。
RegExp对象:返回[object RegExp]。
其他对象:返回[object " + 构造函数的名称 + "]。
也就是说,Object.prototype.toString可以得到一个实例对象的构造函数。
Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"
利用这个特性,可以写出一个比typeof运算符更准确的类型判断函数。
var type = function (o){
var s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"
在上面这个type函数的基础上,还可以加上专门判断某种类型数据的方法。
'Undefined',
'Boolean',
'Function',
'Infinite'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase();
type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true
对象的属性模型
ECMAScript 5对于对象的属性,提出了一个精确的描述模型。
属性的attributes对象,Object.getOwnPropertyDescriptor()
在JavaScript内部,每个属性都有一个对应的attributes对象,保存该属性的一些元信息。使用Object.getOwnPropertyDescriptor方法,可以读取attributes对象。
var o = { p: 'a' };
Object.getOwnPropertyDescriptor(o, 'p')
// Object { value: "a",
writable: true,
enumerable: true,
configurable: true
上面代码表示,使用Object.getOwnPropertyDescriptor方法,读取o对象的p属性的attributes对象。
attributes对象包含如下元信息。
value:表示该属性的值,默认为undefined。
writable:表示该属性的值(value)是否可以改变,默认为true。
enumerable: 表示该属性是否可枚举,默认为true。如果设为false,会使得某些操作(比如for...in循环、Object.keys())跳过该属性。
configurable:表示“可配置性”,默认为true。如果设为false,将阻止某些操作改写该属性,比如,无法删除该属性,也不得改变该属性的attributes对象(value属性除外),也就是说,configurable属性控制了attributes对象的可写性。
get:表示该属性的取值函数(getter),默认为undefined。
set:表示该属性的存值函数(setter),默认为undefined。
Object.defineProperty(),Object.defineProperties()
Object.defineProperty方法允许通过定义attributes对象,来定义或修改一个属性,然后返回修改后的对象。它的格式如下:
Object.defineProperty(object, propertyName, attributesObject)
Object.defineProperty方法接受三个参数,第一个是属性所在的对象,第二个是属性名(它应该是一个字符串),第三个是属性的描述对象。比如,新建一个o对象,并定义它的p属性,写法如下。
var o = Object.defineProperty({}, 'p', {
value: 123,
writable: false,
enumerable: true,
configurable: false
o.p = 246;
// 因为writable为false,所以无法改变该属性的值
需要注意的是,Object.defineProperty方法和后面的Object.defineProperties方法,都有性能损耗,会拖慢执行速度,不宜大量使用。
Object.defineProperty的一个用途,是设置动态属性名。
Object.defineProperty(obj, someFunction(), {value: true});
如果一次性定义或修改多个属性,可以使用Object.defineProperties方法。
var o = Object.defineProperties({}, {
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: { get: function () { return this.p1 + this.p2 },
enumerable:true,
configurable:true
o.p1 // 123
o.p2 // "abc"
o.p3 // "123abc"
上面代码中的p3属性,定义了取值函数get。这时需要注意的是,一旦定义了取值函数get(或存值函数set),就不能将writable设为true,或者同时定义value属性,否则会报错。
var o = {};
Object.defineProperty(o, 'p', {
value: 123,
get: function() { return 456; }
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value,
上面代码同时定义了get属性和value属性,结果就报错。
Object.defineProperty()和Object.defineProperties()的第三个参数,是一个属性对象。它的writable、configurable、enumerable这三个属性的默认值都为false。
writable属性为false,表示对应的属性的值将不得改写。
var o = {};
Object.defineProperty(o, 'p', {
value: "bar"
o.p // bar
o.p = 'foobar';
o.p // bar
Object.defineProperty(o, 'p', {
value: 'foobar',
// TypeError: Cannot redefine property: p
上面代码由于writable属性默认为false,导致无法对p属性重新赋值,但是不会报错(严格模式下会报错)。不过,如果再一次使用Object.defineProperty方法对value属性赋值,就会报错。
configurable属性为false,将无法删除该属性,也无法修改attributes对象(value属性除外)。
var o = {};
Object.defineProperty(o, 'p', {
value: 'bar',
delete o.p
o.p // "bar"
上面代码中,由于configurable属性默认为false,导致无法删除某个属性。
enumerable属性为false,表示对应的属性不会出现在for...in循环和Object.keys方法中。
Object.defineProperty(o, 'p3', {
for (var i in o) {
console.log(i, o[i]);
上面代码中,p3属性是用Object.defineProperty方法定义的,由于enumerable属性默认为false,所以不出现在for...in循环中。
可枚举性(enumerable)
可枚举性(enumerable)用来控制所描述的属性,是否将被包括在for...in循环之中。具体来说,如果一个属性的enumerable为false,下面三个操作不会取到该属性。
for..in循环
Object.keys方法
JSON.stringify方法
因此,enumerable可以用来设置“秘密”属性。
var o = {a: 1, b: 2};
Object.defineProperty(o, 'd', {
enumerable: false
for( var key in o ) console.log( o[key] );
Object.keys(o)
// ["a", "b", "c"]
JSON.stringify(o // =& "{a:1,b:2,c:3}"
上面代码中,d属性的enumerable为false,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但还是可以直接获取它的值。
至于for...in循环和Object.keys方法的区别,在于前者包括对象继承自原型对象的属性,而后者只包括对象本身的属性。如果需要获取对象自身的所有属性,不管enumerable的值,可以使用Object.getOwnPropertyNames方法,详见下文。
考虑到JSON.stringify方法会排除enumerable为false的值,有时可以利用这一点,为对象添加注释信息。
var car = {
color: 'red',
ownerId: 12
var owner = {
name: 'Jack'
Object.defineProperty(car, 'ownerInfo', {value: owner, enumerable: false});
car.ownerInfo // {id: 12, name: "Jack"}
JSON.stringify(car) //
"{"id": 123,"color": "red","ownerId": 12}"
上面代码中,owner对象作为注释,加入car对象。由于ownerInfo属性不可枚举,所以JSON.stringify方法最后输出car对象时,会忽略ownerInfo属性。
这提示我们,如果你不愿意某些属性出现在JSON输出之中,可以把它的enumerable属性设为false。
Object.getOwnPropertyNames()
Object.getOwnPropertyNames方法返回直接定义在某个对象上面的全部属性的名称,而不管该属性是否可枚举。
var o = Object.defineProperties({}, {
p1: { value: 1, enumerable: true },
p2: { value: 2, enumerable: false }
Object.getOwnPropertyNames(o)
// ["p1", "p2"]
一般来说,系统原生的属性(即非用户自定义的属性)都是不可枚举的。
// 比如,数组实例自带length属性是不可枚举的
Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]
// Object.prototype对象的自带属性也都是不可枚举的
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
'valueOf',
'constructor',
'toLocaleString',
'isPrototypeOf',
'propertyIsEnumerable',
'toString']
上面代码可以看到,数组的实例对象([])没有可枚举属性,不可枚举属性有length;Object.prototype对象也没有可枚举属性,但是有不少不可枚举属性。
Object.prototype.propertyIsEnumerable()
对象实例的propertyIsEnumerable方法用来判断一个属性是否可枚举。
var o = {};
o.p = 123;
o.propertyIsEnumerable("p") // true
o.propertyIsEnumerable("toString") // false
上面代码中,用户自定义的p属性是可枚举的,而继承自原型对象的toString属性是不可枚举的。
可配置性(configurable)
可配置性(configurable)决定了是否可以修改属性的描述对象。也就是说,当configurable为false的时候,value、writable、enumerable和configurable都不能被修改了。
var o = Object.defineProperty({}, 'p', {
writable: false,
enumerable: false,
configurable: false
Object.defineProperty(o,'p', {value: 2})
// TypeError: Cannot redefine property: p
Object.defineProperty(o,'p', {writable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(o,'p', {enumerable: true})
// TypeError: Cannot redefine property: p
Object.defineProperties(o,'p',{configurable: true})
// TypeError: Cannot redefine property: p
上面代码首先生成对象o,并且定义属性p的configurable为false。然后,逐一改动value、writable、enumerable、configurable,结果都报错。
需要注意的是,writable只有在从false改为true会报错,从true改为false则是允许的。
var o = Object.defineProperty({}, 'p', {
writable: true
Object.defineProperty(o,'p', {writable: false})
// 修改成功
至于value,只要writable和configurable有一个为true,就可以改动。
var o1 = Object.defineProperty({}, 'p', {
writable: true,
configurable: false
Object.defineProperty(o1,'p', {value: 2})
// 修改成功
var o2 = Object.defineProperty({}, 'p', {
writable: false,
configurable: true
Object.defineProperty(o2,'p', {value: 2})
// 修改成功
可配置性决定了一个变量是否可以被删除(delete)。
{% highlight javascript %}
var o = Object.defineProperties({}, {
p1: { value: 1, configurable: true },
p2: { value: 2, configurable: false }});
delete o.p1 // truedelete o.p2 // false
o.p1 // undefinedo.p2 // 2
{% endhighlight %}
上面代码中的对象o有两个属性,p1是可配置的,p2是不可配置的。结果,p2就无法删除。
需要注意的是,当使用var命令声明变量时,变量的configurable为false。
var a1 = 1;
Object.getOwnPropertyDescriptor(this,'a1')
// Object {
writable: true,
enumerable: true,
configurable: false
而不使用var命令声明变量时(或者使用属性赋值的方式声明变量),变量的可配置性为true。
Object.getOwnPropertyDescriptor(this,'a2')
// Object {
writable: true,
enumerable: true,
configurable: true
// 或者写成
this.a3 = 1;
Object.getOwnPropertyDescriptor(this,'a3')
// Object {
writable: true,
enumerable: true,
configurable: true
上面代码中的this.a3 = 1与a3 = 1是等价的写法。this指的是当前的作用域,更多关于this的解释,参见《面向对象编程》一章。
这种差异意味着,如果一个变量是使用var命令生成的,就无法用delete命令删除。也就是说,delete只能删除对象的属性。
var a1 = 1;
delete a1 // false
delete a2 // true
a2 // ReferenceError: a2 is not defined
可写性(writable)
可写性(writable)决定了属性的值(value)是否可以被改变。
`javascriptvar o = {};
Object.defineProperty(o, "a", { value : 37, writable : false });
o.a // 37o.a = 25;o.a // 37
上面代码将o对象的a属性可写性设为false,然后改变这个属性的值,就不会有任何效果。
这实际上将某个属性的值变成了常量。在ES6中,constant命令可以起到这个作用,但在ES5中,只有通过writable达到同样目的。
这里需要注意的是,当对a属性重新赋值的时候,并不会抛出错误,只是静静地失败。但是,如果在严格模式下,这里就会抛出一个错误,即使是对a属性重新赋予一个同样的值。
关于可写性,还有一种特殊情况。就是如果原型对象的某个属性的可写性为false,那么派生对象将无法自定义这个属性。
```javascript
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
var o = Object.create(proto);
o.foo = 'b';
o.foo // 'a'
上面代码中,对象proto的foo属性不可写,结果proto的派生对象o,也不可以再自定义这个属性了。在严格模式下,这样做还会抛出一个错误。但是,有一个规避方法,就是通过覆盖attributes对象,绕过这个限制,原因是这种情况下,原型链会被完全忽视。
Object.defineProperty(o, 'foo', { value: 'b' });
o.foo // 'b'
存取器(accessor)
除了直接定义以外,属性还可以用存取器(accessor)定义。其中,存值函数称为setter,使用set命令;取值函数称为getter,使用get命令。
return 'getter';
set p(value) {
console.log('setter: ' + value);
上面代码中,o对象内部的get和set命令,分别定义了p属性的取值函数和存值函数。定义了这两个函数之后,对p属性取值时,取值函数会自动调用;对p属性赋值时,存值函数会自动调用。
o.p // "getter"
o.p = 123 // "setter: 123"
注意,取值函数Getter不能接受参数,存值函数Setter只能接受一个参数(即属性的值)。另外,对象也不能与取值函数同名的属性。比如,上面的对象o设置了取值函数p以后,就不能再另外定义一个p属性。
存取器往往用于,某个属性的值需要依赖对象内部数据的场合。
get next() { return this.$n++ },
set next(n) {
if (n &= this.$n) this.$n =
else throw '新的值必须大于当前值';
o.next // 5
o.next = 10;
o.next // 10
上面代码中,next属性的存值函数和取值函数,都依赖于对内部属性$n的操作。
存取器也可以通过Object.defineProperty定义。
var d = new Date();
Object.defineProperty(d, 'month', {
get: function () {
return d.getMonth();
set: function (v) {
d.setMonth(v);
上面代码为Date的实例对象d,定义了一个可读写的month属性。
存取器也可以使用Object.create方法定义。
var o = Object.create(Object.prototype, {
get: function () {
return 'getter';
set: function (value) {
console.log('setter: '+value);
如果使用上面这种写法,属性foo必须定义一个属性描述对象。该对象的get和set属性,分别是foo的取值函数和存值函数。
利用存取器,可以实现数据对象与DOM对象的双向绑定。
Object.defineProperty(user, 'name', {
get: function () {
return document.getElementById('foo').
set: function (newValue) {
document.getElementById('foo').value = newV
configurable: true
上面代码使用存取函数,将DOM对象foo与数据对象user的name属性,实现了绑定。两者之中只要有一个对象发生变化,就能在另一个对象上实时反映出来。
对象的拷贝
有时,我们需要将一个对象的所有属性,拷贝到另一个对象。ES5没有提供这个方法,必须自己实现。
var extend = function (to, from) {
for (var property in from) {
to[property] = from[property];
extend({}, {a: 1})
上面这个方法的问题在于,如果遇到存取器定义的属性,会只拷贝值。
extend({}, { get a(){ return 1 } })
为了解决这个问题,我们可以通过Object.defineProperty方法来拷贝属性。
var extend = function (to, from) {
for (var property in from) {
Object.defineProperty(to, property, Object.getOwnPropertyDescriptor(from, property));
extend({}, { get a(){ return 1 } })
// { get a(){ return 1 } })
这段代码还是有问题,拷贝某些属性时会失效。
extend(document.body.style, {
backgroundColor: "red"
上面代码的目的是,设置document.body.style.backgroundColor属性为red,但是实际上网页的背景色并不会变红。但是,如果用第一种简单拷贝的方法,反而能够达到目的。这提示我们,可以把两种方法结合起来,对于简单属性,就直接拷贝,对于那些通过描述对象设置的属性,则使用Object.defineProperty方法拷贝。
var extend = function (to, from) {
for (var property in from) {
var descriptor = Object.getOwnPropertyDescriptor(from, property);
if (descriptor && ( !descriptor.writable
|| !descriptor.configurable
|| !descriptor.enumerable
|| descriptor.get
|| descriptor.set)) {
Object.defineProperty(to, property, descriptor);
to[property] = from[property];
上面的这段代码,可以很好地拷贝任意属性。
控制对象状态
JavaScript提供了三种方法,精确控制一个对象的读写状态,防止对象被改变。最弱一层的保护是preventExtensions,其次是seal,最强的freeze。
Object.preventExtensions方法
Object.preventExtensions方法可以使得一个对象无法再添加新的属性。
var o = new Object();
Object.preventExtensions(o);
Object.defineProperty(o, "p", { value: "hello" });
// TypeError: Cannot define property:p, object is not extensible.
o.p // undefined
如果是在严格模式下,则会抛出一个错误。
(function () {
'use strict';
// TypeError: Can't add property bar, object is not extensible
不过,对于使用了preventExtensions方法的对象,可以用delete命令删除它的现有属性。
var o = new Object();
Object.preventExtensions(o);
delete o.p;
o.p // undefined
Object.isExtensible方法
Object.isExtensible方法用于检查一个对象是否使用了preventExtensions方法。也就是说,该方法可以用来检查是否可以为一个对象添加属性。
var o = new Object();
Object.isExtensible(o)
Object.preventExtensions(o);
Object.isExtensible(o)
上面代码新生成了一个o对象,对该对象使用Object.isExtensible方法,返回true,表示可以添加新属性。对该对象使用Object.preventExtensions方法以后,再使用Object.isExtensible方法,返回false,表示已经不能添加新属性了。
Object.seal方法
Object.seal方法使得一个对象既无法添加新属性,也无法删除旧属性。
var o = { p:"hello" };
Object.seal(o);
delete o.p;
o.p // "hello"
o.x = 'world';
o.x // undefined
Object.seal还把现有属性的attributes对象的configurable属性设为false,使得attributes对象不再能改变。
var o = { p: 'a' };
// seal方法之前
Object.getOwnPropertyDescriptor(o, 'p')
// Object {value: "a", writable: true, enumerable: true, configurable: true}
Object.seal(o);
// seal方法之后
Object.getOwnPropertyDescriptor(o, 'p')
// Object {value: "a", writable: true, enumerable: true, configurable: false}
Object.defineProperty(o, 'p', { enumerable: false })
// TypeError: Cannot redefine property: p
从上面代码可以看到,使用seal方法之后,attributes对象的configurable就变成了false,然后如果想改变enumerable就会报错。
可写性(writable)有点特别。如果writable为false,使用Object.seal方法以后,将无法将其变成true;但是,如果writable为true,依然可以将其变成false。
var o1 = Object.defineProperty({}, 'p', {writable: false});
Object.seal(o1);
Object.defineProperty(o1,'p',{writable:true})
// Uncaught TypeError: Cannot redefine property: p
var o2 = Object.defineProperty({}, 'p', {writable: true});
Object.seal(o2);
Object.defineProperty(o2,'p',{writable:false})
Object.getOwnPropertyDescriptor(o2, 'p')
/* { value: '',
writable: false,
enumerable: true,
configurable: false } */
上面代码中,同样是使用了Object.seal方法,如果writable原为false,改变这个设置将报错;如果原为true,则不会有问题。
至于属性对象的value是否可改变,是由writable决定的。
var o = { p: 'a' };
Object.seal(o);
o.p = 'b';
o.p // 'b'
上面代码中,Object.seal方法对p属性的value无效,是因为此时p属性的writable为true。
Object.isSealed方法
Object.isSealed方法用于检查一个对象是否使用了Object.seal方法。
var o = { p: 'a' };
Object.seal(o);
Object.isSealed(o) // true
另外,这时isExtensible方法也返回false。
var o = { p: 'a' };
Object.seal(o);
Object.isExtensible(o) // false
Object.freeze方法
Object.freeze方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。
var o = {p:"hello"};
Object.freeze(o);
o.p = "world";
o.p // hello
o.t = "hello";
o.t // undefined
上面代码中,对现有属性重新赋值(o.p = "world")或者添加一个新属性,并不会报错,只是默默地失败。但是,如果是在严格模式下,就会报错。
var o = {p:"hello"};
Object.freeze(o);
// 对现有属性重新赋值
(function () { 'use strict'; o.p = "world";}())
// TypeError: Cannot assign to read only property 'p' of #&Object&
// 添加不存在的属性
(function () { 'use strict'; o.t = 123;}())
// TypeError: Can't add property t, object is not extensible
Object.isFrozen方法
Object.isFrozen方法用于检查一个对象是否使用了Object.freeze()方法。
var o = {p:"hello"};
Object.freeze(o);
Object.isFrozen(o) // true
需要注意的是,使用上面这些方法锁定对象的可写性,但是依然可以通过改变该对象的原型对象,来为它增加属性。
var o = new Object();
Object.preventExtensions(o);
var proto = Object.getPrototypeOf(o);
proto.t = "hello";
一种解决方案是,把原型也冻结住。
var o = Object.seal(
Object.create(Object.freeze({x:1}),
{y: {value: 2, writable: true}})
Object.getPrototypeOf(o).t = "hello";
o.hello // undefined
Array是JavaScript的内置对象,同时也是一个构造函数,可以用它生成新的数组。
作为构造函数时,Array可以接受参数,但是不同的参数,会使得Array产生不同的行为。
// 无参数时,返回一个空数组
new Array() // []
// 单个正整数参数,表示返回的新数组的长度
new Array(1) // [undefined × 1]
new Array(2) // [undefined x 2]
// 单个非正整数参数(比如字符串、布尔值、对象等),
// 则该参数是返回的新数组的成员
new Array('abc') // ['abc']
new Array([1]) // [Array[1]]
// 多参数时,所有参数都是返回的新数组的成员
new Array(1, 2) // [1, 2]
从上面代码可以看到,Array作为构造函数,行为很不一致。因此,不建议使用它生成新数组,直接使用数组的字面量是更好的方法。
var arr = new Array(1, 2);
var arr = [1, 2];
另外,Array作为构造函数时,如果参数是一个正整数,返回的空数组虽然可以取到length属性,但是取不到键名。
Array(3).length // 3
Array(3)[0] // undefined
Array(3)[1] // undefined
Array(3)[2] // undefined
0 in Array(3) // false
1 in Array(3) // false
2 in Array(3) // false
上面代码中,Array(3)是一个长度为3的空数组。虽然可以取到每个位置的键值,但是所有的键名都取不到。
JavaScript语言的设计规格,就是这么规定的,虽然不是一个大问题,但是还是必须小心。这也是不推荐使用Array构造函数的一个理由。
Array对象的静态方法
isArray方法
Array.isArray方法用来判断一个值是否为数组。它可以弥补typeof运算符的不足。
var a = [1, 2, 3];
typeof a // "object"
Array.isArray(a) // true
上面代码表示,typeof运算符只能显示数组的类型是Object,而Array.isArray方法可以对数组返回true。
Array实例的方法
以下这些Array实例对象的方法,都是数组实例才能使用。如果不想创建实例,只是想单纯调用这些方法,可以写成[].method.call(调用对象,参数) 的形式,或者Array.prototype.method.call(调用对象,参数)的形式。
valueOf方法,toString方法
valueOf方法返回数组本身。
var a = [1, 2, 3];
a.valueOf() // [1, 2, 3]
toString 方法返回数组的字符串形式。
var a = [1, 2, 3];
a.toString() // "1,2,3"
var a = [1, 2, 3, [4, 5, 6]];
a.toString() // "1,2,3,4,5,6"
push(),pop()
push方法用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
var a = [];
a.push(1) // 1
a.push('a') // 2
a.push(true, {}) // 4
a // [1, 'a', true, {}]
上面代码使用push方法,先后往数组中添加了四个成员。
如果需要合并两个数组,可以这样写。
var a = [1, 2, 3];
var b = [4, 5, 6];
Array.prototype.push.apply(a, b)
a.push.apply(a, b)
// 上面两种写法等同于
a.push(4, 5, 6)
a // [1, 2, 3, 4, 5, 6]
push方法还可以用于向对象添加元素,添加后的对象变成类似数组的对象,即新加入元素的键对应数组的索引,并且对象有一个length属性。
var a = {a: 1};
[].push.call(a, 2);
a // {a:1, 0:2, length: 1}
[].push.call(a, [3]);
a // {a:1, 0:2, 1:[3], length: 2}
pop方法用于删除数组的最后一个元素,并返回该元素。注意,该方法会改变原数组。
var a = ['a', 'b', 'c'];
a.pop() // 'c'
a // ['a', 'b']
对空数组使用pop方法,不会报错,而是返回undefined。
[].pop() // undefined
push和pop结合使用,就构成了“后进先出”的栈结构(stack)。
join(),concat()
join方法以参数作为分隔符,将所有数组成员组成一个字符串返回。如果不提供参数,默认用逗号分隔。
var a = [1, 2, 3, 4];
a.join(' ') // '1 2 3 4'
a.join(' | ') // "1 | 2 | 3 | 4"
a.join() // "1,2,3,4"
通过call方法,join方法(即Array.prototype.join)也可以用于字符串。
Array.prototype.join.call('hello', '-')
// "h-e-l-l-o"
concat方法用于多个数组的合并。它将新数组的成员,添加到原数组的尾部,然后返回一个新数组,原数组不变。
['hello'].concat(['world'])
// ["hello", "world"]
['hello'].concat(['world'], ['!'])
// ["hello", "world", "!"]
除了接受数组作为参数,concat也可以接受其他类型的值作为参数。它们会作为新的元素,添加数组尾部。
[1, 2, 3].concat(4, 5, 6)
// [1, 2, 3, 4, 5, 6]
[1, 2, 3].concat(4, [5, 6])
如果不提供参数,concat方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是如果数组成员包括复合类型的值(比如对象),则新数组拷贝的是该值的引用。
var obj = { a:1 };
var oldArray = [obj];
var newArray = oldArray.concat();
obj.a = 2;
newArray[0].a // 2
上面代码中,原数组包含一个对象,concat方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变。事实上,只要原数组的成员中包含对象,concat方法不管有没有参数,总是返回该对象的引用。
concat方法也可以用于将对象合并为数组,但是必须借助call方法。
[].concat.call({ a: 1 }, { b: 2 })
// [{ a: 1 }, { b: 2 }]
[].concat.call({ a: 1 }, [2])
// [{a:1}, 2]
[2].concat({a:1})
shift(),unshift()
shift方法用于删除数组的第一个元素,并返回该元素。注意,该方法会改变原数组。
var a = ['a', 'b', 'c'];
a.shift() // 'a'
a // ['b', 'c']
shift方法可以遍历并清空一个数组。
var list = [1, 2, 3, 4, 5, 6];
while (item = list.shift()) {
console.log(item);
list // []
push和shift结合使用,就构成了“先进先出”的队列结构(queue)。
unshift方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
var a = ['a', 'b', 'c'];
a.unshift('x'); // 4
a // ['x', 'a', 'b', 'c']
reverse方法用于颠倒数组中元素的顺序,使用这个方法以后,返回改变后的原数组。
var a = ['a', 'b', 'c'];
a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]
slice方法用于提取原数组的一部分,返回一个新数组,原数组不变。
它的第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。
arr.slice(start_index, upto_index);
var a = ['a', 'b', 'c'];
a.slice(0) // ["a", "b", "c"]
a.slice(1) // ["b", "c"]
a.slice(1, 2) // ["b"]
a.slice(2, 6) // ["c"]
如果slice方法的参数是负数,则表示倒数计算的字符串位置。
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]
如果参数值大于数组成员的个数,或者第二个参数小于第一个参数,则返回空数组。
var a = ['a', 'b', 'c'];
a.slice(4) // []
a.slice(2, 1) // []
slice方法的一个重要应用,是将类似数组的对象转为真正的数组。
Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 })
// ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);
上面代码的参数都不是数组,但是通过call方法,在它们上面调用slice方法,就可以把它们转为真正的数组。
splice方法用于删除原数组的一部分成员,并可以在被删除的位置添加入新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。
splice的第一个参数是删除的起始位置,第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。
arr.splice(index, count_to_remove, addElement1, addElement2, ...);
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2) // ["e", "f"]
a // ["a", "b", "c", "d"]
上面代码从原数组位置4开始,删除了两个数组成员。
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
上面代码除了删除成员,还插入了两个新成员。
如果只是单纯地插入元素,splice方法的第二个参数可以设为0。
var a = [1, 1, 1];
a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]
如果只提供第一个参数,则实际上等同于将原数组在指定位置拆分成两个数组。
var a = [1, 2, 3, 4];
a.splice(2) // [3, 4]
a // [1, 2]
sort方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。
['d', 'c', 'b', 'a'].sort()
// ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort()
// [1, 2, 3, 4]
[11, 101].sort()
// [101, 11]
[,111].sort()
// [1, 111]
上面代码的最后两个例子,需要特殊注意。sort方法不是按照大小排序,而是按照对应字符串的字典顺序排序,所以101排在11的前面。
如果想让sort方法按照自定义方式排序,可以传入一个函数作为参数,表示按照自定义方法进行排序。该函数本身又接受两个参数,表示进行比较的两个元素。如果返回值大于0,表示第一个元素排在第二个元素后面;其他情况下,都是第一个元素排在第二个元素前面。
[,111].sort(function (a,b){
return a -
// [111, ]
{ name: "张三", age: 30 },
{ name: "李四", age: 24 },
{ name: "王五", age: 28
].sort(function(o1, o2) {
return o1.age - o2.
{ name: "李四", age: 24 },
{ name: "王五", age: 28
{ name: "张三", age: 30 }
ECMAScript 5 新加入的数组方法
ECMAScript 5新增了9个数组实例的方法,分别是map、forEach、filter、every、some、reduce、reduceRight、indexOf和lastIndexOf。其中,前7个与函数式(functional)操作有关。
这些方法可以在数组上使用,也可以在字符串和类似数组的对象上使用,这是它们不同于传统数组方法的一个地方。
在用法上,这些方法的参数是一个函数,这个作为参数的函数本身又接受三个参数:数组的当前元素elem、该元素的位置index和整个数组arr(详见下面的实例)。另外,上下文对象(context)可以作为第二个参数,传入forEach(), every(), some(), filter(), map()方法,用来绑定函数运行时的上下文。
对于不支持这些方法的老式浏览器(主要是IE 8及以下版本),可以使用函数库,或者和。
Array.prototype.map()
map方法对数组的所有成员依次调用一个函数,根据函数结果返回一个新数组。
var numbers = [1, 2, 3];
numbers.map(function (n) { return n + 1 });
// [2, 3, 4]
// [1, 2, 3]
上面代码中,numbers数组的所有成员都加上1,组成一个新数组返回,原数组没有变化。
map方法接受一个函数作为参数。该函数调用时,map方法会将其传入三个参数,分别是当前成员、当前位置和数组本身。
[1, 2, 3].map(function(elem, index, arr) {
return elem *
// [1, 4, 9]
上面代码中,map方法的回调函数的三个参数之中,elem为当前成员的值,index为当前成员的位置,arr为原数组([1, 2, 3])。
map方法不仅可以用于数组,还可以用于字符串,用来遍历字符串的每个字符。但是,不能直接使用,而要通过函数的call方法间接使用,或者先将字符串转为数组,然后使用。
var upper = function (x) { return x.toUpperCase() };
[].map.call('abc', upper)
// [ 'A', 'B', 'C' ]
'abc'.split('').map(upper)
// [ 'A', 'B', 'C' ]
其他类似数组的对象(比如document.querySelectorAll方法返回DOM节点集合),也可以用上面的方法遍历。
map方法还可以接受第二个参数,表示回调函数执行时this所指向的对象。
var arr = ['a', 'b', 'c'];
[1, 2].map(function(e){
return this[e];
// ['b', 'c']
上面代码通过map方法的第二个参数,将回调函数内部的this对象,指向arr数组。
map方法通过键名,遍历数组的所有成员。所以,只要数组的某个成员取不到键名,map方法就会跳过它。
var f = function(n){ return n + 1 };
[1, undefined, 2].map(f) // [2, NaN, 3]
[1, null, 2].map(f) // [2, 1, 3]
[1, , 2].map(f) // [2, undefined, 3]
上面代码中,数组的成员依次包含undefined、null和空位。前两种情况,map方法都不会跳过它们,因为可以取到undefined和null的键名。第三种情况,map方法实际上跳过第二个位置,因为取不到它的键名。
1 in [1, , 2] // false
上面代码说明,第二个位置的空位是取不到键名的,因此map方法会跳过它。
下面的例子会更清楚地说明这一点。
[undefined, undefined].map(function (){
console.log('enter...');
// enter...
// enter...
Array(2).map(function (){
console.log('enter...');
// [undefined x 2]
上面代码中,Array(2)生成的空数组是取不到键名的,因此map方法根本没有执行,直接返回了Array(2)生成的空数组。
Array.prototype.forEach()
数组实例的forEach方法与map方法很相似,也是遍历数组的所有成员,执行某种操作,但是forEach方法没有返回值,一般只用来操作数据。如果需要有返回值,一般使用map方法。
function log(element, index, array) {
console.log('[' + index + '] = ' + element);
[2, 5, 9].forEach(log);
// [0] = 2
// [1] = 5
// [2] = 9
从上面代码可以看到,forEach方法和map方法的参数格式是一样的,第一个参数都是一个函数。该函数接受三个参数,分别是当前元素、当前元素的位置(从0开始)、整个数组。
forEach方法会跳过数组的空位。
var log = function(n) {
console.log(n + 1);
[1, undefined, 2].forEach(log)
[1, null, 2].forEach(log)
[1, , 2].forEach(log)
上面代码中,forEach方法不会跳过undefined和null,但会跳过空位。
forEach方法也可以接受第二个参数,用来绑定回调函数的this关键字。
var out = [];
[1, 2, 3].forEach(function(elem) {
this.push(elem * elem);
out // [1, 4, 9]
上面代码中,空数组out是forEach方法的第二个参数,结果,回调函数内部的this关键字就指向out。
filter方法
filter方法依次对所有数组成员调用一个测试函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组。
[1, 2, 3, 4, 5].filter(function (elem) {
return (elem & 3);
上面代码将大于3的原数组成员,作为一个新数组返回。
再看一个例子。
var arr = [0, 1, 'a', false];
arr.filter(Boolean)
// [1, "a"]
上面例子中,通过filter方法,返回数组arr里面所有布尔值为true的成员。
filter方法的参数函数可以接受三个参数,第一个参数是当前数组成员的值,这是必需的,后两个参数是可选的,分别是当前数组成员的位置和整个数组。
[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
return index % 2 === 0;
// [1, 3, 5]
上面代码返回原数组偶数位置的成员组成的新数组。
filter方法还可以接受第二个参数,指定测试函数所在的上下文对象(即this对象)。
var Obj = function () {
this.MAX = 3;
var myFilter = function (item) {
if (item & this.MAX) {
var arr = [2, 8, 3, 4, 1, 3, 2, 9];
arr.filter(myFilter, new Obj())
// [8, 4, 9]
上面代码中,测试函数myFilter内部有this对象,它可以被filter方法的第二个参数绑定。上例中,myFilter的this绑定了Obj对象的实例,返回大于3的成员。
some(),every()
这两个方法类似“断言”(assert),用来判断数组成员是否符合某种条件。
some方法对所有元素调用一个测试函数,只要有一个元素通过该测试,就返回true,否则返回false。
var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
return elem &= 3;
上面代码表示,如果存在大于等于3的数组成员,就返回true。
every方法对所有元素调用一个测试函数,只有所有元素通过该测试,才返回true,否则返回false。
var arr = [1, 2, 3, 4, 5];
arr.every(function (elem, index, arr) {
return elem &= 3;
上面代码表示,只有所有数组成员大于等于3,才返回true。
从上面的代码可以看到,some和every的使用方法与map和forEach一致,参数完全一模一样。也就是说,它们也可以使用第二个参数,用来绑定函数中的this关键字。
reduce方法,reduceRight方法
reduce方法和reduceRight方法的作用,是依次处理数组的每个元素,最终累计为一个值。这两个方法的差别在于,reduce对数组元素的处理顺序是从左到右(从第一个成员到最后一个成员),reduceRight则是从右到左(从最后一个成员到第一个成员),其他地方完全一样。
reduce方法的第一个参数是一个处理函数。该函数接受四个参数,分别是:
初始变量,默认为数组的第一个元素值。函数第一次执行后的返回值作为函数第二次执行的初始变量,依次类推。
当前变量,如果指定了reduce函数或者reduceRight函数的第二个参数,则该变量为数组的第一个元素的值,否则,为第二个元素的值。
当前变量对应的元素在数组中的序号(从0开始)。
这四个参数之中,只有前两个是必须的,后两个则是可选的。
[1, 2, 3, 4, 5].reduce(function(x, y){
console.log(x,y)
return x+y;
//最后结果:15
上面代码未指定reduce函数的第二个参数,因此,第一轮中,x为1,y为2。然后,第二轮开始,x为上一轮的返回值,y为3。依次执行,直到遍历完数组中所有元素。所以最终结果为15。
利用reduce方法,可以写一个数组求和的sum方法。
Array.prototype.sum = function (){
return this.reduce(function (partial, value){
return partial +
[3,4,5,6,10].sum()
如果要对初始变量指定初值,可以把它放在reduce方法的第二个参数。
[1, 2, 3, 4, 5].reduce(function(x, y){
return x+y;
上面代码指定参数x的初值为10,所以数组元素从10开始累加,最终结果为25。
由于reduce方法依次处理每个元素,所以实际上还可以用它来搜索某个元素。比如,下面代码是找出长度最长的数组元素。
function findLongest(entries) {
return entries.reduce(function (longest, entry) {
return entry.length & longest.length ? entry :
indexOf 和 lastIndexOf
indexOf方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回-1。
var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1
indexOf方法还可以接受第二个参数,表示搜索的开始位置。
['a', 'b', 'c'].indexOf('a', 1) // -1
上面代码从位置1开始搜索字符a,结果为-1,表示没有搜索到。
lastIndexOf方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1。
var a = [2, 5, 9, 2];
a.lastIndexOf(2)
a.lastIndexOf(7)
注意,如果数组中包含NaN,这两个方法不适用。
[NaN].indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1
这是因为这两个方法内部,使用严格相等运算符(===)进行比较,而NaN是唯一一个不等于自身的值。
上面这些数组方法之中,有不少返回的还是数组,所以可以链式使用。
var users = [{name:"tom", email:""},
{name:"peter", email:""}];
.map(function (user){ return user. })
.filter(function (email) { return /^t/.test(email); })
.forEach(alert);
包装对象和Boolean对象
在JavaScript中,“一切皆对象”,数组和函数本质上都是对象,就连三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。
所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
上面代码根据原始类型的值,生成了三个对象,与原始值的类型不同。这用typeof运算符就可以看出来。
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false
JavaScript设计包装对象的最大目的,首先是使得JavaScript的“对象”涵盖所有的值。其次,使得原始类型的值可以方便地调用特定方法。
包装对象的构造函数
Number、String和Boolean这三个原生对象,既可以当作构造函数使用(即加上new关键字,生成包装对象实例),也可以当作工具方法使用(即不加new关键字,直接调用),这相当于生成实例后再调用valueOf方法,常常用于将任意类型的值转为某种原始类型的值。
Number(123) // 123
String("abc") // "abc"
Boolean(true) // true
工具方法的详细介绍参见第二章的《数据类型转换》一节。
包装对象实例的方法
包装对象实例可以使用Object对象提供的原生方法,主要是 valueOf 方法和 toString 方法。
(1)valueOf方法
valueOf方法返回包装对象实例对应的原始类型的值。
new Number(123).valueOf()
new String("abc").valueOf()
new Boolean("true").valueOf()
(2)toString方法
toString方法返回该实例对应的原始类型值的字符串形式。
new Number(123).toString()
new String("abc").toString()
new Boolean("true").toString()
原始类型的自动转换
原始类型的值,可以自动当作对象调用,即调用各种对象的方法和参数。这时,JavaScript引擎会自动将原始类型的值转为包装对象,在使用后立刻销毁。
比如,字符串可以调用length属性,返回字符串的长度。
'abc'.length // 3
上面代码中,abc是一个字符串,本身不是对象,不能调用length属性。JavaScript引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型的自动转换。
var str = 'abc';
str.length // 3
var strObj = new String(str)
// String {
0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"
strObj.length // 3
上面代码中,字符串abc的包装对象有每个位置的值、有length属性、还有一个内部属性[[PrimitiveValue]]保存字符串的原始值。这个[[PrimitiveValue]]内部属性,外部是无法调用,仅供ValueOf或toString这样的方法内部调用。
这个临时对象是只读的,无法修改。所以,字符串无法添加新属性。
var s = 'Hello World';
s.x = 123;
s.x // undefined
上面代码为字符串s添加了一个x属性,结果无效,总是返回undefined。
另一方面,调用结束后,临时对象会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果想要为字符串添加属性,只有在它的原型对象String.prototype上定义(参见《面向对象编程》一章)。
这种原始类型值可以直接调用的方法还有很多(详见后文对各包装对象的介绍),除了前面介绍过的valueOf和toString方法,还包括三个包装对象各自定义在实例上的方法。。
'abc'.charAt === String.prototype.charAt
上面代码表示,字符串abc的charAt方法,实际上就是定义在String对象实例上的方法(关于prototype对象的介绍参见《面向对象编程》一章)。
如果包装对象与原始类型值进行混合运算,包装对象会转化为原始类型(实际是调用自身的valueOf方法)。
new Number(123) + 123
new String("abc") + "abc"
// "abcabc"
自定义方法
三种包装对象还可以在原型上添加自定义方法和属性,供原始类型的值直接调用。
比如,我们可以新增一个double方法,使得字符串和数字翻倍。
String.prototype.double = function (){
return this.valueOf() + this.valueOf();
"abc".double()
Number.prototype.double = function (){
return this.valueOf() + this.valueOf();
(123).double()
上面代码在123外面必须要加上圆括号,否则后面的点运算符(.)会被解释成小数点。
但是,这种自定义方法和属性的机制,只能定义在包装对象的原型上,如果直接对原始类型的变量添加属性,则无效。
var s = "abc";
s.p = 123;
s.p // undefined
上面代码直接对支付串abc添加属性,结果无效。
Boolean对象
Boolean对象是JavaScript的三个包装对象之一。作为构造函数,它主要用于生成布尔值的包装对象的实例。
var b = new Boolean(true);
typeof b // "object"
b.valueOf() // true
上面代码的变量b是一个Boolean对象的实例,它的类型是对象,值为布尔值true。这种写法太繁琐,几乎无人使用,直接对变量赋值更简单清晰。
Boolean实例对象的布尔值
特别要注意的是,所有对象的布尔运算结果都是true。因此,false对应的包装对象实例,布尔运算结果也是true。
if (new Boolean(false)) {
console.log("true");
if (new Boolean(false).valueOf()) {
console.log("true");
} // 无输出
上面代码的第一个例子之所以得到true,是因为false对应的包装对象实例是一个对象,进行逻辑运算时,被自动转化成布尔值true(所有对象对应的布尔值都是true)。而实例的valueOf方法,则返回实例对应的原始类型值,本例为false。
Boolean函数的类型转换作用
Boolean对象除了可以作为构造函数,还可以单独使用,将任意值转为布尔值。这时Boolean就是一个单纯的工具方法。
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean('') // false
Boolean(NaN) // false
Boolean(1) // true
Boolean('false') // true
Boolean([]) // true
Boolean({}) // true
Boolean(function(){}) // true
Boolean(/foo/) // true
上面代码中几种得到true的情况,都值得认真记住。
使用not运算符(!)也可以达到同样效果。
!!undefined // false
!!null // false
!!0 // false
!!'' // false
!!NaN // false
!!1 // true
!!'false' // true
!![] // true
!!{} // true
!!function(){} // true
!!/foo/ // true
综上所述,如果要获得一个变量对应的布尔值,有多种写法。
var a = "hello world";
new Boolean(a).valueOf() // true
Boolean(a) // true
!!a // true
最后,对于一些特殊值,Boolean对象前面加不加new,会得到完全相反的结果,必须小心。
if (Boolean(false))
console.log('true'); // 无输出
if (new Boolean(false))
console.log('true'); // true
if (Boolean(null))
console.log('true'); // 无输出
if (new Boolean(null))
console.log('true'); // true
Number对象
Number对象是数值对应的包装对象,可以作为构造函数使用,也可以作为工具函数使用。
作为构造函数时,它用于生成值为数值的对象。
var n = new Number(1);
typeof n // "object"
上面代码中,Number对象作为构造函数使用,返回一个值为1的对象。
作为工具函数时,它可以将任何类型的值转为数值。
Number(true) // 1
上面代码将布尔值true转为数值1。Number对象的工具方法,详细介绍参见上一章的《数据类型转换》一节。
Number对象的属性
Number对象拥有以下一些属性。
Number.POSITIVE_INFINITY:正的无限,指向Infinity。
Number.NEGATIVE_INFINITY:负的无限,指向-Infinity。
Number.NaN:表示非数值,指向NaN。
Number.MAX_VALUE:表示最大的正数,相应的,最小的负数为-Number.MAX_VALUE。
Number.MIN_VALUE:表示最小的正数(即最接近0的正数,在64位浮点数体系中为5e-324),相应的,最接近0的负数为-Number.MIN_VALUE。
Number.MAX_SAFE_INTEGER:表示能够精确表示的最大整数,即0991。
Number.MIN_SAFE_INTEGER:表示能够精确表示的最小整数,即-0991。
}

我要回帖

更多关于 getarguments返回null 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信