六 标准库

1 Object 对象

教程:https://wangdoc.com/javascript/stdlib/object.html

1.1 概述

JS 原生提供 Object 对象(注意起首的 O是大写的)。JS 的所有其他对象都继承自 Object 对象,即那些对象都是 Object 的实例。
Object 对象的原生方法分为两类:Object 本身的方法和Object的实例方法。

(1)Object对象本身的方法
所谓 Object 本身的方法就是直接定义在 Object对象的方法。

Object.print = function(o){console.log(o)};

(2)Object的实例方法
所谓实例方法就是定义在 Object 原型对象 Object.prototype 上的方法。它可以被 Object 实例直接使用。

Object.prototype.print = ()=>{
  console.log(this);
}

var obj = new Object();
obj.print();

凡是定义在 Object.prototype 对象上面的属性和方法,将被所有实例对象共享。

以下先介绍 Object 作为函数的用法,然后再介绍Object对象的原生方法,分成对象自身的方法(“静态方法”)和实例方法两部分。

1.2 Object()

Object 本身是一个函数,可以当作工具方法使用,将任意值转为对象。这个方法常用于保证某个值一定是对象。

如果参数为空(或者为 undefinednull), Object 返回一个空对象。

var obj = Object();
// 等同于
var obj = Object(undefined);
var obj = Object(null);

obj instanceof Object // true

上面代码的含义,是将 undefinednull转为对象,结果得到了一个空对象 obj
instanceof 运算符用来验证,一个对象是否为指定的构造函数的实例。obj instanceof Object 返回true, 就表示 obj 对象是Object 的实例。
如果参数是原始类型的值,Object方法将其转为对应的包装对象的实例。

var obj = Object(1);
obj instanceof Object // true
obj instanceof Number // true

var obj = Object("foo");
obj instanceof Object // true
obj instanceof String // true

var obj = Object(true);
obj instanceof Object // true
obj instanceof Boolean // true

上面代码中,Object 函数的参数是各种原始类型的值,转换成对象就是原始类型值对应的包装对象。
如果 Object 方法的参数是一个对象,它总是返回该对象,即不用转化。

var arr = [];
var obj = object(arr); // 返回原数组
obj === arr; // true

var value = {};
var obj = Object(value); // 返回原对象
obj === value // true

var fn = ()=>{};
var obj = Object(fn); // 返回原函数
obj === fn // true

利用这一点,可以判断一个判断变量是否为对象的函数。

function isObject(value){
  return value === Object(value);
}

1.3 Object 构造函数

Object 不仅可以当工具函数使用,还可以当作构造函数使用,即前面可以使用 new 命令。
Object 构造函数的主要目的,直接通过它来生产新的对象。

var obj = new Object();

注意,通过 var obj = new Object() 的写法生产新对象,与字面量的写法var obj = {} 是等价的。或者说,后者只是前者的一种简便写法。

Object构造函数的用法与工具方法很相似,几乎一模一样。使用时,可以接受一个参数,如果该参数是一个对象,则直接返回这个对象;如果是一个原始类型的值,则直接返回该值对应的包装对象。

var o1 = {a: 1};
var o2 = new Object(o1);
o1 === o2; // true

var obj = new Object(123);
obj instanceof Number // true

用法虽然相似,但是 Object(value)new Object(value) 两者的语义是不同的,Object(value) 表示将value转成一个对象,new Object(value) 则表示新生一个对象,它的值是 value

1.4 Object 的静态方法

所谓“静态方法”,是指部署在Object对象自身的方法。

1.4.1 Object.keys(), Object.getOwnPropertyNames()

Object.keys 方法和 Object.getOwnPropertyNames 方法都用来遍历对象的属性。
Object.keys方法的参数是一个对象,返回一个数组。该数组的成员都是该对象自身的(而不是继承的)所有属性名。

var obj = {
  p1: 123,
  p2: 456
};
Object.keys(obj)
// ["p1", "p2"]

Object.getOwnPropertyNames 方法与Object.keys类似,也是接受一个对象作为参数,返回一个数组,包含了该对象自身所有属性名。

Object.getOwnPropertyNames(obj) // ["p1", "p2"]

对于一般属性来说,Object.keysObject.getOwnPropertyNames 返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。Object.keys 方法只返回可枚举的属性,Object.getOwnPropertyNames 方法还返回不可枚举的属性名。

var a = ["hello", "world"];
Object.keys(a) // ["0", "1"]
Object.getOwnPropertyNames(a)
// ["0", "1", "length"]

1.4.2 其他方法

除了上面提到的两个方法,Object 还有不少其他的静态方法,将在后文逐一介绍。
(1)对象属性模型的方法

  • Object.getOwnPropertyDescriptor() : 获取某个属性的描述对象。
  • Object.defineProperty(): 通过描述对象,定义某个属性。
  • Object.defineProperties(): 通过描述对象,定义多个属性。
    (2)控制对象状态的方法
  • Object.preventExtensions():防止对象扩展。
  • Object.isExtensible(): 判断对象是否可扩展。
  • Object.seal(): 禁止对象配置。
  • Object.isSealed(): 判断一个对象是否可配置。
  • Object.freeze(): 冻结一个对象。
  • Object.isFrozen(): 判断一个对象是否被冻结。
    (3)原型链相关方法
  • Object.create(): 该方法可以指定原型对象和属性,返回一个新的对象。
  • Object.getPrototypeOf(): 获取对象的 Prototype 对象。

1.5 Object 的实例方法

除了静态方法,还有不少方法定义在 Object.prototype 对象。它们称为实例方法,所有Object的实例对象都继承了这些方法。
Object实例对象的方法,主要有以下六个:

  • Object.prototype.valueOf(): 返回当前对象对应的值
  • Object.prototype.toString(): 返回当前对象对应的字符串形式
  • Object.prototype.toLocaleString(): 返回当前对象对应的本地字符串形式
  • Object.prototype.hasOwnProperty(): 判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。
  • Object.prototype.isPrototypeOf():判断当前对象是否为另一个对象的原型。
  • Object.prototype.propertyIsEnumerable(): 判断某个属性是否可枚举。

1.5.1 Object.prototype.valueOf()

valueOf 方法的作用是返回一个对象的“值”,默认情况下返回对象本身。

var obj = new Object();
obj.valueOf() === obj 
// true

上面代码比较 obj.valueOf()obj 本身,两者是一样的。
valueOf 方法的主要途径是,JS 自动类型转换时会默认调用这个方法。

var obj = new Object();
1 + obj 
// "1[object object]"

上面代码将对象obj与数字1相加,这时 JS 就会默认调用 valueOf() 方法,求出 obj 的值再与 1 相加。所以,如果自定义 valueOf 方法,就可以得到想要的结果。

var obj = new Object();
obj.valueOf = (){}=>{return 2;};

1 + obj // 3

上面代码自定义 obj 对象的 valueOf 方法,于是 1 + obj 就得到了 3

1.5.2 Object.prototype.toString()

toString 方法的作用是返回一个对象的字符串形式,默认情况下返回类型字符串。

var o1 = new Object();
o1.toString() // "[object Oject]"

字符串形式的 [object Object]本身没有太大的用处,但是通过自定义 toString 方法,可以让对象在自动类型转换时,得到想要的字符串形式。

var obj = new Object();

obj.toString = ()=>{
  return "hello";
}

obj + " " + "world" 
// "hello world"

数组、字符串、函数、Date 对象调用 toString 方法,并不会返回 [object obect], 是因为它们都自定义了 toString 方法,覆盖了 Object.prototype.toString 方法。

[1,2,3].toString() // "1,2,3"
"123".toString() // "123"
(()=>{return 123;}).toString() 
// "(()=>{return 123;})"
(new Date()).toString() // "Tue May 10 2016 09:11:31"

1.5.3 toString() 的应用:判断数据类型

Object.prototype.toString 方法返回对象的类型字符串,因此可以用来判断一个值的类型。

var obj = {};
obj.toString() // "[object Object]"

返回的字符串 object Object, 第二个 Object 表示该值的构造函数。这是一个十分有用的判断数据类型的方法。

由于实例对象可能会自定义 toString 方法,覆盖掉 Object.prototype.toString 方法,为了得到类型字符串,最好直接使用 Object.prototype.toString 方法。通过函数的 call 方法,可以在任意值上调用这个方法,帮助我们判断这个值的类型。

Object.prototype.toString.call(value)

不同数据类型的 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 函数基础上,还可以加上专门判断某种类型数据的方法。

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
}

["Null", "Undefined", "Object", "Array", "String", "Number", "Boolean", "Function", "RegExp"].forEach(
  (t)=>{
    type["is"+t] = (o){
        return type(o) === t.toLowerCase();
    }   
})

type.isObject({}) // true
type.isNumber(NaN) // true

JS 这个特性非常任性,一个对象可以随便加方法。

var m = ()=>{}
m.a = true;
m.a // true

这么短短的三句,就为 m 添加了一个对象a。看来被 typeof 归为对象的,不是瞎归类,它们确实有了对象应该有的一些特性。

1.5.4 Object.prototype.toLocaleString()

Object.prototype.toLocalString 方法与 toString 的返回结果相同,也是返回一个值的字符串形式。

var obj = {};
obj.toString(obj) // "[object Object]"
obj.toLocaleString(obj) // "[object Object]"

这个方法的主要作用是留出一个接口,让各种不同的对象实现自己版本的tolocaleString, 用来返回针对某些对象的特定的值。

  • Array.prototype.toLocaleString()
  • Number.prototype.toLocaleString()
  • Date.prototype.toLocaleString()
    举例来说,日期的实例对象的 toStringtoLocaleString 返回值就不一样,而且 toLocaleString 的返回值跟用户设定的所在对象有关。
var date = new Date();
date.toString()  
// "Thu Aug 16 2018 16:18:31 GMT+0800 (香港标准时间)"
date.toLocaleString() // "2018/8/16"

1.5.5 Object.prototype.hasOwnProperty()

Object.prototype.hasOwnProperty 方法接受一个字符串作为参数,返回一个布尔值,表示该实例对象自身是否具有该属性。

var obj = {
  p : 123
}
obj.hasOwnProperty('p') // true
obj.hasOwnProperty("toString") // false

2 属性描述对象

https://wangdoc.com/javascript/stdlib/attributes.html

2.1 概述

JS 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等。这个内部数据结构称为 “属性描述对象”(attribute object)。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。

下面是属性描述对象的一个例子。

{
  value: 123,
  writeable: false,
  enumerable: true,
  configurable: false,
  get: undefined,
  set: undefined
}

属性描述对象提供6个元属性。
(1) value
value 是该属性的属性值,默认为 undefined
(2) writable
writable 是一个布尔值,表示属性值是否可改变(即是否可写),默认为 true
(3) enumerable
enumerable 是一个布尔值,表示该属性是否可遍历,默认为 true。如果设为false,会使得某些操作(比如 for ... in 循环、Object.keys())跳过该属性。
(4) configurable
configurable 是一个布尔值,表示该属性是否可遍历,默认为 true。如果设为false,会使得某些操作(比如for ... in循环、Object.keys())跳过该属性。
(5) get
get 是一个函数,表示该属性的取值函数(getter),默认为 undefined
(6) set
set 是一个函数,表示该属性的存值函数(set),默认为 undefined

2.2 Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor 方法可以获取属性描述对象。它的第一个参数是一个对象,第二个参数是一个字符串,对应该对象的某个属性名。

var obj = {p: "a"};
Object.getOwnPropertyDescriptor(obj, "p")
// Object {
  configurable: true,
  enumerable: true,
  writable: true
}

上面代码中,Object.getOwnPropertyDescriptor 方法获取 obj.p 的属性描述对象。
注意,Object.getOwnPropertyDescriptor 方法只能用于对象自身的属性,不能用于继承的属性。

var obj = {p : "a"};
Object.getOwnPropertyDescriptor(obj, "toString")
// undefined

2.3 Object.getOwnPropertyNames()

Object.getOwnPropertyNames() 方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否是可遍历的。

var obj = Object.defineProperties({},{
  p1: {value: 1, enumerable: true},
  p2: {value: 2, enumerable: false}
});

Object.getOwnPropertyNames(obj)
// ["p1", "p2"]

在上面代码中,obj.p1 是可遍历的,obj.p2 是不遍历的。Object.getOwnPropertyNames 会将它们都返回。
这和 Object.keys 的行为不同,Object.keys 只返回对对象自身的可遍历属性的全部属性名。

Object.keys(obj) // ["p1"]

Object.keys([]) // []
Object.getOwnPropertyNames([]) // ["length"]
Object.getOwnPropertyNames(()=>{}) // ["length", "name"]
Object.getOwnPropertyNames({}) // []

Object.keys(Object.prototype)  // []
Object.getOwnPropertyNames(Object.prototype)
// [
  "constructor",
  "__defineGetter__",
  "__defineSetter__",
  "hasOwnProperty",
  "__lookupGetter__",
  "__lookupSetter__",
  "isPrototypeOf",
  "propertyIsEnumerable",
  "toString" 
]

通过 prototype 对象定义的属性和方法会被该类所有的实例共享,所以一般使用 prototype 定义方法。这个 prototype 好比静态属性。

2.4 Object.defineProperty(), Object.defineProperties()

Object.defineProperty 方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象,它的用法如下:

Object.definePropperty(object, propertyName, attributesObject)

Object.defineProperty 方法接受三个参数,依次如下:

  • 属性所在的对象
  • 属性名(它应该是个字符串)
  • 属性值
    举例:
var obj = Object.defineProperty({}, "p", {
  value : 123,
  writable: false,
  enumerable: true,
  configurable: false
});

obj.p // 123
obj.p = 456;
obj.p // 123

上面代码中,Object.defineProperty 方法定义了 obj.p 属性。由于属性描述对象的 writable 属性为 false,

2.5 Object.prototype.propertyIsEnumerable()

实例对象的 propertyIsEnumerable 方法返回一个布尔值,用来判断某个属性是否可遍历的。

var obj = {};
obj.p = 123;

obj.propertyIsEnumerable("p") // "true"
obj.propertyIsEnumerable("toString") // false

上面代码中,obj.p 是可遍历的,而继承自原型对象的 obj 是不可运行的。

2.6 元属性

属性描述对象的各个属性称为“元属性”,因为它们可以看作控制属性的属性。

2.6.1 value

value 属性是目标属性的值。

var obj = {};
obj.p = 123;

Object.getOwnPropertyDescriptor(obj, "p").value
// 123
Object.defineProperty(obj, "p", {value : 246})
// obj.p // 246

上面代码是通过 value 属性,读取或改写 obj.p 的例子。

2.6.2 writable

writable 属性是一个布尔值,决定了目标属性的值value是否可以被改变。

var obj = {};
Object.defineProperty(obj, "a", {
  value: 77,
  writable: false
});

obj.a // 77
obj.a = 25;
obj.a // 77

上面代码中,obj.a 的 writable 属性是 false。然后,改变 obj.a 的值,不会有任何效果。

在正常模式下,对 writablefalse 的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对 a属性重新赋予一个同样的值。

"use strict"
var obj = {};

Object.defineProperty(obj, "a", {
  value: 37,
  writable: false
});

obj.a = 33;
// Uncaught TypeError : Cannot assign to read only property "a" of object 

上面代码是严格模式,对 obj.a 任何赋值行为都会报错。

如果原型对象的某个属性的 writablefalse, 那么子对象将无法自定义这个属性。

var proto = Object.defineProperty({}, "foo", {
  value: "a",
  writable: false
});

// proto 的子对象
var obj = Object.create(proto);
obj.foo = "b";
obj.foo // "a"
obj // {}

上面代码中,proto 是原型对象,它的 foo属性不可写。obj对象继承proto,也不可以再自定义这个属性了。如果是严格模式,这样做还会抛出一个错误。

但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽略。

var proto = Object.defineProperty({}, "foo", {
  value: "a",
  writable: false
});

var obj = Object.create(proto);
Object.defineProperty(obj, "foo",{
  value: "b"
});

obj.foo // "b"

2.6.3 enumerable

enumerable (可遍历性)返回一个布尔值,表示目标属性是否可遍历。
JS 的早期版本,for ... in 循环是基于 in 运算符的。我们知道,in 运算符不管某个属性是否对象自身的还是继承的,都会返回 true

var obj = {};
"toString" in obj // true

上面代码中,toString 不是 obj 对象自身的属性,但是 in 运算符也返回了 true,这导致了toString 属性也会被 for ... in 循环遍历。

这显然不太合理,toString 不是obj自身的属性,但是仍然被遍历到了。所以,引入了 “可遍历性”这个概念。只有可遍历的属性,才会被for... in循环遍历,同时还规定toString 这一类实例对象继承的原生属性,都是不可遍历的,这样就保证了for ... in 循环的可使用性。

具体来说,如果一个属性的enumerablefalse, 下面三个操作不会去到该属性:

  • for ... in 循环
  • Object.keys 循环
  • JSON.stringify 循环
    因此,可以使用 enumerable 设置 “秘密” 属性。
var obj = {};
Object.defineProperty(obj, "x", {
  value: 123,
  enumerable: false
});

obj.x // 123
for(var key in obj){
  console.log(key)
}
// undefined
Object.keys(obj) // []
JSON.stringify(obj) // "[]"

上面代码中,obj.x 属性的 enumerable 为 false,所以一般的遍历操作都无法获取该属性,使得它有点像秘密属性,但不是整整的私有属性,还是可以直接获取它的值。

注意,for ... in 循环包括继承的属性,Object.keys 方法不包含继承的属性。如果需要获取自身所有的属性,不管是否可以遍历,可以使用Object.getOwnPropertyNames 方法。

另外,JSON.stringify 方法会排除 enumerablefalse 的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的enumerable 设为 false

2.6.4 configurable

configurable (可配置性)返回一个布尔值,决定了是否可以修改属性描述对象。也就是说,configurable 为 false 时,value、writable、enumerable 和 configurable 都不能被修改。

var obj = Object.defineProperty({}, "p"., {
  value: 1,
  writable: false,
  enumerable: false,
  configurable: false
});

Object.defineProperty(obj, "p", {value: 2})
Object.defineProperty(obj, "p", {writable: true})
Object.defineProperty(obj, "p", {enumerable: true})
Object.defineProperty(obj, "p", {configurable: true})
// TypeError: Cannot redefine property: p
// 上面代码中,`obj.p` 的 `configurable` 为 `false`。

注意,writable 只有在false 改为 true 会报错,true 改为 false 是允许的。

var obj = Object.defineProperty({}, "p", {
  writable: true,
  configurable: false
});

Object.defineProperty(obj, "p", {writable: false})

至于 value,只要writableconfigurable 有一个是true,就允许改动。

var o1= Object.defineProperty({}, "p", {
  value: 1,
  writable: true,
  configurable: false
})

Object.defineProperty(o1, "p", {value: 2})
// 修改成功

var o2 = Object.defineProperty({}, "p", {
  value: 1,
  writable: false,
  configurable: true
})

Object.defineProperty(o2, "p", {value: 2})
// 修改成功

另外在 configurable为 false,直接目标属性赋值,不会报错,但是不会成功。

var obj  = Object.defineProperty({}, "p", {
  value: 1,
  configurable: false
})

obj.p = 2;
obj.p // 1

可见 configurable 为 false 的情况下,不会报错,也不会成功。如果在严格模式下,还会报错。
可配置性决定目标属性是否可以被删除(delete)。

var obj = Object.defineProperties({}, {
  p1: {value: 1, configurable: true},
  p2: {value: 2, configurable: false}
})

delete obj.p1 // true
delete obj.p2 // false

obj.p1 // undefined
obj.p2 // 2

上面代码中,obj.p1 的configurable 是 true,所以可以被删除,obj.p2 无法被删除

2.7 存取器

除了直接定义以为,属性还可以用存储器(accessor)定义。其中,存值函数称为setter, 使用属性描述对象set函数;取值函数称为getter,使用属性描述对象的 get属性。
—— 这就是 Java 那一套呗。

一旦对目标属性定义了存取器,那么存取的时候,都将执行对应的函数。利用这个功能,可以实现许多高级功能,比如某个属性禁止赋值。

var obj = Object.defineProperty({}, "p", {
  get: function(){
    return "getter";
  },
  set: function(value){
    console.log("setter: " + value)
  }
})

obj.p // "getter"
obj.p = 123
// "setter: 123" 

上面代码中, obj.p 定义了 getset 属性。obj.p 取值时,就会调用 get; 赋值时,就会调用 set
JS 还提供了存取器的另一种写法。

var obj = {
  get p(){
    return "getter";
  }
  set p(value){
    console.log("setter: " + value);
  }
}

上面的写法与定义属性描述对象是等价的,而且使用更广泛。
注意,取值函数get不能接受参数,存值函数set只能接受一个参数(即属性的值)。
村取值往往用于,属性的值依赖对象内部数据的场合。

var obj = {
  $n : 5,
  get next(){
     return $n;
  },
  set next(n){
    this.$n = n;
  }
}

obj.next // 5
obj.next = 10;
obj.next // 10

2.8 对象的拷贝

有时,我们需要将一个对象的所有属性,拷贝到另一个对象,可以用下面方法实现。

var extend = function(to, from){
  for(var property in from){
    to[property] = from[property];
  }
  return to;
}

extend({}, {
  a: 1
})
// {a: 1}

上面这个方法的问题在于,如果遇到存取器定义的属性,会只拷贝值。

extend({}, {
  get a(){return 1}
})
// {a: 1}

为了解决这个问题,我们可以通过 Object.defineProperty 方法来拷贝属性。

var extend = function (to, from) {
  for (var property in from) {
    if (!from.hasOwnProperty(property)) continue;
    Object.defineProperty(
      to,
      property,
      Object.getOwnPropertyDescriptor(from, property)
    );
  }

  return to;
}

extend({}, { get a(){ return 1 } })

// 感觉这个例子不太对,因为 for ... in 无法获取 { get a(){ return 1 } } 的get 方法

2.9 控制对象状态

有时需要冻结对象的读写状态,防止对象被改变。JS 提供了三种冻结方法,最弱的是一种Object.preventExtensions, 其次是Object.seal, 最强的是 Object.freeze

2.9.1 Object.preventExtensions()

Object.preventExtensions 方法可以使得一个对象无法再添加新属性。

var obj = new Object();
Object.preventExtensions(obj);

Object.defineProperty(obj, "p", {
  value: "hello"
});
// TypeError: Cannot define property:
obj.p = 1;
obj.p
// undefined

上面代码中,obj 对象经过 Object.preventExtensions以后,无法添加新属性。

2.9.2 Object.isExtensible()

Object.isExtensible 方法可以用来检测一个对象是否使用了 Object.preventExtension.

var obj = new Object();

Object.isExtensible(obj) // true
Object.preventExtensions(obj);
Object.isExtensible(obj) // false

2.9.3 Object.seal()

Object.seal 使得一个方法无法添加新属性,也无法删除旧的属性。

var obj = {p : "hello"};
Object.seal(obj);

delete obj.p // false
obj.p // "hello"

obj.x = "a";
obj.x // undefined

seal 实际上是把属性描述对象的 configurable 属性设置为false,因此属性描述对象不再能改变了。

var obj = {p: "a"};
// seal 方法之前
Object.getOwnPropertyDescriptor(obj, "p");
// {value: 1, writable: true, enumerable: true, configurable: true}
Object.seal(obj);
Object.getOwnPropertyDescriptor(obj, "p");
// {value:1, writable: true, enumerable: true, configurable: false}

Object.defineProperty(obj, "p", {enumerable: false})
// TypeError: Cannot redefine property: p

上面的代码,使用Object.seal 方法之后,属性描述的 configurable 就变为 false,然后改变 enumerable 就会报错。

Object.seal 只是禁止新增或删除属性,但是不影响属性的修改。

var obj = {p : "a"};
Object.seal(obj);
obj.p = "b";
obj.p //  "b"

上面代码中,Object.seal 方法对 p 属性的 value无效,因为此时 p 属性的可写性由writable 决定。

2.9.4 Object.isSealed()

Object.isSealed 方法用于检查一个对象是否使用了 Object.seal 方法。

var obj = {p: "a"};

Object.seal(obj);
Object.isSealed(obj) // true

这时, Object.isExtensible 方法也返回 false

var obj = {p: a};

Object.seal(obj);
Object.isExtensible(obj) // false

2.9.5 Object.freeze()

Object.freeze 方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。

var obj = {p : "hello"};
Object.getOwnPropertyDescriptor(obj, "p");
// {value: "hello", writable: true, enumerable: true, configurable: true}
Object.freeze(obj);
Object.getOwnPropertyDescriptor(obj, "p");
// {value: "hello", writable: false, enumerable: true, configurable: false}

obj.t = "hello";
obj.t // undefined

delete obj.p // false
obj.p // "hello"

上面代码中,对obj对象进行Object.freeze 以后,修改、删除、新增属性都无效了。如果在严格模式下,还会抛出异常。

2.9.6 Object.isFrozen()

Object.isFrozen 方法用于检查一个对象是否使用了 Object.freeze 方法。

var obj = {
  p: "hello"
};

Object.freeze(obj);
Object.isFrozen(obj) // true
Object.isExtensible(obj) // false
Object.isSealed(obj) // true

Object.isFrozen 的用途是,确认某个对象没有被冻结后,再对它的属性赋值。

var obj = {
  p: "hello"
};

Object.freeze(obj);

if (!Object.isFrozen(obj)){
  obj.p = "helloWorld";
}

2.9.7 局限性

上面三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来对对象增加属性。

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
proto.t = "hello";
obj.t // hello

上面代码中,对象obj 本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够在 obj 上读到。

一种解决方案是,把obj的原型也冻住。

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
Object.preventRExtensions(proto);

proto.t = "hello";
obj.t // undefined

另一个局限是,如果属性值是对象,上面这些方法只能冻住属性指向的对象,而不能冻住对象本身的内容。

var obj = {
  foo : 1,
  bar : [1,2,3];
}

Object.freeze(obj);
obj.bar.push(2);
obj.bar
// [1,2,3,2]

3 Array 对象

3.1 构造函数

Array 是 JS 的原生对象,同时也是一个构造函数,可以用它生成新的数组。

var arr = new Array(2);
arr.length // 2
arr // [ empty x 2]

上面代码中,Array 构造函数的参数是2,表示生成一个两个成员的数组,每个位置是空值。
如果没有使用 new,运行结果也是一样的。

var arr = new Array(2);
// 等同于
arr = new Arrray(2);

Array 构造函数有一个很大的缺陷,就是不同参数,会导致它的行为不一致。
当参数是单个且是Number时,表示长度;其余表示数组成员

// 无参数时,返回一个空数组
new Array() // 空数组
// 单个正整数参数,表示返回的新数组的长度
new Array(1) // [ empty ]
// 非整数的数组作为参数会报错
new Array(2.3) // Uncaught RangeError: Invalid array length
// 单个非数值作为参数,则该参数是返回的新数组的成员
new Array("abc") // ["abc"]
// 多参数时,所有参数都是返回数组的新成员
new Array(1.2,2.3)

因为Array 作为构造函数,行为不一致,所以使用数组字面量更好。

var a = [1.2];

注意,使用 Array构造函数时,如果参数是一个正整数,返回数组的成员都是空位。虽然读取的时候返回 undefined,但实际上该位置没有任何值。虽然可以取到length属性,但是取不到键名。

var a = new Array(3);
var b = [undefined, undefined, undefined];

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

Object.keys(a) // []
Object.keys(b) // ["1", "2", "3"];

a[0] // undefined
b[0] // undefined

读取键值的时候,a 和 b 都返回 undefined,但是 a 的键位都是空的,b 的键位是有值的。

3.2 静态方法

3.2.1 Array.isArray()

Array.isArray 方法返回一个布尔值,表示参数是否为数组。它可以弥补 typeof 运算符的不足。

var arr = [1,2,3];

typeof arr // "object"
Array.isArray(arr) // true

3. 3 实例方法

3.3.1 valueOf(), toString()

valueOf 方法是一个所有对象都拥有的方法,表示对该对象求值。不同对象的 valueOf 不一致,数组的 valueOf 返回数组本身。

var arr = [1,2,3];
arr.valueOf() // [1,2,3]

toString 也是对象的通用方法,数组的 toString 方法返回数组的字符串形式。

var arr = [1,2,3, [4,5,6]];
arr.toString()
// "1,2,3,4,5,6,7,8,"

3.3.2 push, pop()

push 方法添加一个或多个元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。

var arr = [];
arr.push(true, {})
// 2

pop 用于删除数组的最后一个元素,并返回该元素。

arr = [1,2,3,4];
arr.pop() // 4

对于空数组使用 pop 方法,不会报错,而是返回 undefined.

[].pop() // undefined

push 和 pop 结合使用,就构成了"后进先出"的栈结构。

var arr = [];
arr.push(1);
arr.push(2,3);
arr // [1,2,3]
arr.pop() 3

3.3.3 shift(), unshift()

shift 方法用于删除数组的第一个元素,并返回该元素。

var a = ["a", "b", "c"];
a.shift() // "a"
a // ["b", "c"]

shift 方法可以遍历并清空一个数组。

// 阮老师写的这个有问题
var list = [1,2,4,false,5];
var item;
while(item = list.shift()){
  console.log(item);
}
list // 5

shift 和 push 相结合就成了“先进先出”队列结构。

unshift 方法用于在数组头部添加元素,并返回添加新元素后的数组长度。

var arr = [];
arr.unshift(1,2);
arr
// [1,2]

unshift 可以添加多个参数,这些参数都会添加到目标数组头部。

var arr = [1,2];
arr.unshift(3,4)
arr;
// [3,4,1,2]

3.3.4 join()

join 方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分割。

var a = [1,2,3,4];
a.join()
// "1,2,3,4"
a.join(" ")
// "1 2 3 4"

如果数组成员是 undefinednull或空位,会被转成空字符串。

var arr = new Array(4);
arr.join()
// ",,,"
var arr2 = [undefined, null, null]
arr2.join("/")
"//"

通过 call 方法,这个方法也可以用于字符串或类似数组的对象。

Array.prototype.join.call("hello", "-");
// "h-e-l-l-o"

// obj 是类似数组的对象,这个概念前面提到过的 
var  obj = {0:"a",1:"b",length:2}
Array.prototype.join.call(obj, "-");
// "a-b"

3.3.5 concat()

concat 方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后面,然后返回一个新数组,原数组不变。

["hello"].concat([1,2,3])
// ["hello",1,2,3]

[1].concat({1:"a"},{2:"b"})
// [1, {1:"a"}, {2:"b"}] 

除了数组作为参数,concat 也接受其他类型的值作为参数,添加到目标数组的尾部。

[1,2,3].concat(4,5,6)
// [1,2,3,4,5,6]

如果数组成员包括对象,concat方法返回的是当前数组的一个浅拷贝。所谓 “浅拷贝”,是指新数组拷贝的是对象的引用。

// 没有对象时是深拷贝,即修改返回的数组,不会影响原来的数组
var w = [1,2,3];
var m = w.concat();
m.push(5)
m // [1,2,3,5]
w // [1,2,3]

// 有对象时,是深拷贝
var w = [1,2,[3,4]];
var m = w.concat();
m[2].push(222);
m // [1,2,[3,4,222]];
w // [1,2,[3,4,222]];

3.3.6 reverse()

reverse 方法用于颠倒排列数组元素,返回改变后的数组。

var a = ["a", "b", "c"];
a.reverse()
a // ["c", "b", "a"]

这个为什么没办法被 call 调用,有点不理解。

3.3.7 slice() 用于切片

slice 方法用于提取目标数组的一部分,返回一个新数组,原数组不变。

arr.slice(start, end)

它的第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。

var a  = ["a","b","c","d"];
a.slice(0); // ["a","b","c","d"]
a.slice(1,3); // ["b","c"]
a.slice(); // ["a","b","c","d"]

如果slice 的参数是负数,则是倒着切片。
如果参数不合理,比如起始位置的参数大于长度,或者第二个参数小于第一个参数,则返回一个空数组(——这当然要排除一正一负的情况)

slice 方法一个重要作用,是将类似数组的对象转为真正的数组。

Array.prototype.slice.call({0:"a", 1:"b", 2:"c", length:3})
// ["a", "b", "c"]

Array.prototype.slice.call(document.querySelectorAll("div"))
// [....]

上面代码的参数都不是真正的数组,但是通过 call 方法,在它们上面调用 slice 方法,就可以把它们转为真正的数组。

3.3.8 splice()

链接:https://wangdoc.com/javascript/stdlib/array.html#splice
splice 方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。

arr.splice(start, count, addElement1, addElement2, ...)

splice 的第一个参数是删除的起始位置(从 0 开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。

var a = [1,2,3,4,5];
a.splice(2,2);
a // [1,2,5]

删除两个成员,再插入两个新成员。

var a = [1,2,3,4,5];
a.splice(2, 0, "a", "b");
// [1,2,"a","b",3,4,5]

如果只提供第一个参数,等同于将原数组在指定位置分为两个数组。

var a = [1, 2, 3, 4];
a.splice(2);  // [3, 4]
a // [1, 2]

3.3.9 sort()

sort 方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。

var w = ["a", "d", "b", "c"];
w.sort()
w
// ["a", "b", "c", "d"]

sort 方法不是按照大小排序,而是按照字典排序。也就说,数值会被先转成字符串,再按照字典顺序进行比较,所以如果排列数值会有点奇怪。

如果想让 sort 方法按照自定义方式排序,可以传入一个函数作为参数。

[10111, 1101, 111].sort((a, b)=>{
  return a -b ;
})
// [-5, 0, 1, 2, 3]

对于对象,自定义比较函数。

[{name: "zhangsan", age: 30},
  {name: "lisi", age: 24},
  {name: "wangwu", age: 28}].sort((a, b)=>{
    return a.age - b.age;
})

3.3.10 map()

map 方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。原数组不改变。

var numbers = [1, 2, 3];
numbers.map((n)=>{
  return n + 1;
});
// [2,3,4]

map 方法接受一个函数作为参数。该函数调用时,map 方法向它传入三个参数:当前成员、当前位置和数组本身。

[1,2,3].map((element, index, array)=>{
  console.log(element, index, array);
  return element * index;
})

map 方法还可以接受第二个参数,用来绑定回调函数内部的 this 变量。

var arr = ["a", "b", "c"];
[0,1,2].map((element)=>{
  return this[e];
}, arr)
// 这是我运行出来的结果,和 阮的不一样
// [undefined, undefined, undefined]

如果数组有空位,map 方法的回调函数在这个位置不会执行,会跳过数组的空位。

var f = function(n) {return "a"};
[1, undefined, 2].map(f) // ["a", "a", "a"]
[1, null, 2].map(f) // ["a", "a", "a"]
[1,,2].map(f) // ["a", , "a"]

map 方法不会跳过 undefinednull,但是会跳过空位。

3.3.11 forEach()

forEach 方法与 map 方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach 方法不返回值,只用来操作数据。
forEach的用法与 map 一致,参数是一个函数,该函数有三个参数:当前值、当前值的位置、整个数组。

[1,2,3].forEach((ele, index, arr)=>{
  console.log(ele, index, arr);
})

也可以接受第二个参数,绑定参数函数的 this 变量。

var out = [];
[1,2,3].forEach((ele)=>{
  this.push(ele * ele);
}, out);

out // [1, 4, 9]
// 阮一峰这么写是对的,之前不对是火箭头函数与 function 匿名函数的区别

forEach 方法无法中断执行,总是会把所有成员遍历完。如果希望符合某种条件就中断,应该使用 for 循环。—— 这点以前不知道

[1,2,3,4].forEach((ele)=>{
  (ele) => {
    console.log(ele);
    if (ele > 2){
        break;
      }
  }
})

//SyntaxError: Illegal break statement

forEach 方法也会跳过数组空位。

var log  = (ele)=>{
  console.log(ele);
}
[1, undefined, 2].forEach(log) // 2 NaN 3
[1, 1, 2].forEach(log) // 2 2 NaN
[1,,2].forEach // 2 3

3.3.12 filter

filter 方法用于过滤数组成员,满足条件的成员组成一个新数组返回。
它的参数是一个函数,所有数组成员依次执行该函数,返回 true 的成员组成一个新数组返回。

[1,2,3,4,5].filter((elem)=>{return (ele > 3)})
// [4, 5]

filter 方法参数函数可以接受三个参数:当前成员,当前位置和整个数组。

[1,2,3,4,5].filter(
  (ele, index, arr)=>{
    return ele * index > 0;
  }
)

filter 方法还可以接受第二个参数,用来绑定参数函数内部的this 变量

var obj = {max : 3};
// this 应该只能存活在一个上下文的环境中
var myFilter = function(item){
  if (item > this.max){
    return true;
  }
}

[1,2,3,4,4,5,6].filter(myFilter, obj);

myFilter 内部的this变量,它可以被filter 方法中的第二个参数 obj 绑定。

3.3.13 some(), every()

some 和 every 类似于assert,返回一个布尔值,表示判断数组成员是否符合某种条件。

它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值。

some 方法是只要一个成员返回值是true,则整个some方法返回值就是true,否则返回false

// 注意这个匿名函数必须有返回值才行,也就是必须有 return, 否则就相当于返回 undefined ,也就是返回 false
[1,2,3,4,5,56].some((ele, index, arr)=>{return ele > arr[index-1]})

every 方法是所有成员的返回值都是 true,则整个every方法才返回 true,否则返回 false.

var arr = [1,2,3,4,5,6];
arr.every((ele, index, arr)=>{
  return elem >= -3;
})
// true

注意,对于空数组,some方法返回false, every 方法返回true,回调函数都不会执行。

function isEven(x){
  return x % 2 === 0;
}

[].some(isEven) // false
[].every(isEven) // true

3.3.14 reduce(), reduceRight()

reduce 方法和 reduceRight 方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce 是从左到右处理(从第一个成员到最后一个成员),reduceRight 则是从右到左(从最后一个成员到第一个成员),其他完全一样。

[1,2,3,4,5].reduce((a, b)=>{
  console.log(a, b);
  return a + b;
})

reduce 方法和reduceRight方法的第一个参数都是一个函数。该函数接受以下四个参数。

  1. 累积变量,默认为数组的第一个成员
  2. 当前变量,默认为数组的第二个成员
  3. 当前位置(从 0 开始)
  4. 原数组
    如果要对累计变量指定初始值,可以把它当在 reduce 方法和reduceRight 方法的第二个参数。
[1,2,3,4,5].reduce((a,b)=>{
  return a + b
}, 10);

// 25

上面的代码指定 a 的起始值是10,所以数组从10开始累加,最终结果是 25.
第二个参数相当于设置了默认值,处理空数组时尤其有用。

function  add(prev, cur){
  return prev + cur;
}

[].reduce(add) 
// TypeError: Reduce of empty array with no initial value
[].reduce(add, 1)
// 1

reduce 作用于 1个空数组时会报错,只有当a有初始值时才会返回默认值。

function substract(prev, cur){
  return prev - cur;
}

[3, 2, 1].reduce(substract) // 0
[3, 2, 1].reduceRight(substract) // -4

由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员。

["aaa", "vv", "ccd"].reduce((prev, cur)=>{
  return prev.length > cur.length ? prev.length : cur.length;
},"");

为什么能返回最大长度?
因为 reduce 第一次遍历的时候,把 "aaa" 和 "vv" 进行比较,找出了最大值 "aaa". 进行第二次比较的时候,"aaa" 就是 prev 了,拿来和 "ccd" 进行比较。所以返回了 "aaa"。
其实这种找最大长度的,并没有考虑如果有多个符合最大长度怎么办。
我觉得这也是为什么 reduce 啊这类函数式在真实项目中用的比较少的缘故,因为真实项目很复杂,拖泥带水,if 来 if 去,很少能让这么一个轻飘飘的 reduce 就能搞定的。

3.3.15 indexOf(), lastIndexOf

indexOf 方法返回给定元素在数组中第一次出现的位置,如果没有出现返回 -1 。

var  a = ["a", "b", "c", "d"];
a.indexOf("b"); // 1
a.indexOf("bb") // -1

indexOf 方法还可以接受第二个参数,表示搜索的开始位置。

var a =  ["a", "b", "c", "d"];
a.indexOf("a", 1) // -1

lastIndexOf 方法返回给定元素在数组中最后一次出现的位置,如果没有出现返回 -1

a.lastIndexOf("c") // 2

注意,这两个方法不能用来搜索 NaN 的位置,即它们无法确定数组成员是否包含 NaN

[NaN].indexOf(NaN) // -1

这是因为在这两个方法内部,使用严格运算符(===)进行比较,而 NaN 是唯一一个与自身不相等的值。

3.3.16 链式使用

上面这些数组方法之中,有不少返回的还是数组,所以可以使用链接。

var users = [
  {name: 'tom', email: "tom@qq.com"},
  {name: "peter", email: "peter@qq.com"}
]

users.map((user)=>{
  return user.email;
}).filter((email)=>{
  return /^t/.test(email);
}).forEach(console.log);
// "tom@example.com"

4 包装对象

4.1 定义

对象是 JS 语言最主要的数据类型,三种原始类型的值 —— 数值、字符串、布尔值,也会自动转为对象,也就是原始类型的 “包装对象”。

所谓 “包装对象”,就是分别与数值、字符串、布尔值相对于的 NumberStringBoolean 三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。

var v1 = new Number(123);
var v2 = new String("abc");
var v3 = new Boolean(true);

上面代码中,基于原始类型的值,生成了三个对应的包装对象。

typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"

v1 === 123 // false
v2 === "abc" // false
v3 === true // false

把原始值转化成对象,就和 JAVA 中把一些值转化为对象一样,使得原始值能够调用对象的某些方法。

NumberStringBoolean 如果不作为构造函数调用(即调用时不加 new),常常用于将任意类型的值转为数值、字符串和布尔值。

Number(123) // 123
String("abc") // "abc"
Boolean(true) // true

总结一下,这三个对象作为构造函数使用时,可以将原始类型的值转为对象;作为普通函数使用时,可见将任意类型的值,转为原始类型。

4.2 实例方法

三种包装对象各自提供了许多实例方法。这里介绍他们共同具有、从Object对象继承的方法:valueOftoString

4.2.1 valueOf()

valueOf 方法返回包装对实例对应的原始类型的值。

new Number(123).valueOf() // 123
new String("abc").valueOf() "abc"
new Boolean(true).valueOf() // true

4.2.2 toString()

toString 方法返回对应的字符串形式。

new Number(123).toString() // "123"
new String("abc").toString() // "abc"
new Boolean(true).toString() // "true"

4.3 原始类型与实例对象的自动转换

y原始类型的值,可以自动当做包装对象调用,即调用包装对象的属性和方法。这点和 Java 是相似的,叫做装箱机制。

比如,字符串可以调用 length 属性,返回字符串长度。

"abc".length // 3

abc 是一个字符串,本身不是对象,不能调用 length 属性。JS 引擎自动将其转为包装类型,在这个对象上调用 length 属性。调用结束后,这个临时对象就会被销毁。这叫做原始类型与实例对象的自动转换。

var str = "abc";
str.length // 3

// 等同于 
var strObj = new String(str)
strObj.length // 3

自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。

var s = "Hello World";
s.x = 123;
s.x // undefined

上面代码为字符串 s 添加了一个 x 属性,结果无效,总是返回 undefined

另一方面,调用结束后,包装对象的实例会自动销毁。这意味着,下一次调用字符串的属性时,实际上调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象 String.prototype 上定义。

4.4 自定义方法

除了原生的实例方法,包装对象还可以自定义方法和属性,供原始类型的值直接调用。

比如,我们可以新增一个 double 方法,使得字符串和数字翻倍。

String.prototype.double = function (){
  return this.valueOf() + this.valueOf();
// this.valueOf() * 2 
// 我本来写成了这样子,但是是不行的,因为会变成 NaN。这点和 Python 是不同的,在 
JS 中这种运算优先满足乘法。
}

Number.prototype.double = function(){
  return this.valueOf() + this.valueOf();
}

// 数值原始类型调用的时候,需要括号括起来, 否则点运算符(`.`)会被解释成小数点。
(123).valueOf() // 246

这种自定义方法和属性,只能定义在包装对象的原型上(也就是类上),如果直接对原始类型的变量添加属性,则无效。

var s = "abc";

s.p = 123;
s.p // undefined

这是因为包装对象是自动生成的,赋值后自动销毁,所以实际上调用的是一个新的包装对象。

5 Boolean 对象

5.1 概述

Boolean 对象是 JS 的三个包装对象之一。作为构造函数,它主要用于生成布尔值的包装对象实例。

var b = new Boolean(true);
typeof b // "object"
b.valueOf() // true

注意,false 对应的包装对象实例,布尔运算结果也是 true

var f = new Boolean(false);
Boolean(f); 
// true, 这点容易理解,因为 f 是一个对象。
f.valueOf();
// false 

5.2 Boolean 函数的类型转换作用

https://wangdoc.com/javascript/stdlib/boolean.html
Boolean 函数除了可以作为构造函数,还可以单独使用,将任意值转为布尔值。这时,Boolean 就是一个单纯的工具方法。
以下 6 种的布尔值是 false

Boolean(null) // false
Boolean(NaN) // false
Boolean(undefined) // false
Boolean(0) // false
Boolean("") // false
Boolean(false) // false

// 这些是 true,因为它们是对象
Boolean([]) // true
Boolean({}) // true

// 函数的布尔值也是 true
Boolean(function(){}) // true
Boolean(/foo/) // true

使用双重的否运算符(!)也可以将任意值转为对应的布尔值。

!!undefined // false
!!null // false

由于,JS 的这些构造函数加不加new都可以正常使用,所以一定要分清是构造新的对象,还是要进行类型转化,因为可能会得到完全相反的结果。

if(Boolean(false)){
  console.log("---->")
}
// 无输出

if (new Boolean(false)){
  console.log("----->")
}
// ----->

6 Number 对象

https://wangdoc.com/javascript/stdlib/number.html

6.1 概述

Number 对象是数值对于的包装对象,可以作为构造函数使用,也可以作为工具函数。
作为构造函数时,它用于生成值为数值的对象。

var n = new Number(1);
typeof n // "Object"

工具函数时,进行类型转化

Number(NaN) // NaN

6.2 静态属性

Number 对象拥有以下一些静态属性(即直接定义在 Number 对象上的属性,而不是定义在实例上的属性)。

  • Number.POSITIVE_INFINITY: 正的无限,指向 Infinity
  • Number.NEGATIVE_INFINITY: 负的无限,指向 -Infinity
  • Number.NaN: 表示非数值,指向 NaN
  • Number.MIN_VALUE: 表示最小的正数(即最接近0 的正数,在64位浮点数体系中为 5e-324),相应的,最接近0 的负数为 -Number.MIN_VALUE
  • Number.MAX_SAFE_INTEGER: 表示能精确表示的最大整数,即 9007199254740991
  • Number.MIN_SAFE_INTEGER: 表示能够精确表示的最小整数,即 -9007199254740991
Number.POSITIVE_INFINITY // Infinity
Number.NEGATIVE_INFINITY // -Infinity
Number.NaN // NaN

Number.MAX_VALUE // 1.797

6.3 实例方法

Number 对象有 4 个实例方法,都跟将数值转换成指定格式有关。

6.3.1 Number.prototype.toString()

Number 对象部署了自己的 toString 方法,用来将一个数值转为字符串形式。

(10).toString() // "10"

toString 方法可以接受一个参数,表示输出的进制。如果省略这个参数,默认将数值先转为十进制,再输出字符串;否则,就根据参数指定的进制,将一个数字转化成某个进制的字符串。

(10).toString(2)  // "1010"
(10).toString(8) //  "12"
(10).toString(16) // "16"

数字一定要放在括号里,后面的点表示调用对象的属性。如果不加括号,这个点会被
JS 引擎解释成小数点,从而报错。
如果不想加括号,还有以下其他方法。

10..toString(2) // "1010"
10 .toString(2) // "1010"
10.0.toString(2) // "1010"

这意味着,可以直接对一个小数使用 toString 方法。

10.5.toString()

通过方括号运算符也可以调用 toString 方法。

10["toString"](2) // "1010"

6.3.2 Number.prototype.toFixed()

toFixed 方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串。

(10).toFixed(2) // "10.00"

toFixed 方法的参数为小数位数,有效范围是 0 到 20,超出这个范围将 RangeError .

6.3.3 Number.prototype.toExponential()

toExponential 方法用于将一个数转为科学计数法的形式。

(10).toExponential() // "1e+1"
(10).toExponential(1) // "1.0e+1"

toExponential 方法的参数是小数点后有效数字的位数,范围是0到20,超出会抛 RangeError

6.3.4 Number.prototype.toPrecision()

toPrecision 方法用于将一个数转为指定位数的有效数字。

(10).toPrecision(4) // "10.00"
(10).toPrecision(5) // "10.000"

6.4 自定义方法

与其他对象一样,Number.prototype 对象上面可以自定义方法,被 Number 的实例继承。

Number.prototype.add = function(x){
  return this + x ;
}
(8).add(2) // 10

由于 add 有返回值,所以可以进行链式运算。

Number.prototype.subtract = function (x){
  return this - x;
}
(8).add(2).subtract(4) // 6 

还可以部署更加复杂的方法。

// 阮的例子,其实就是 Python 中的 range
// 必须使用 function 匿名函数
Number.prototype.range = function (){
  var result = [];
  for(var i = 0; i < this; i++){
    result.push(i);
  }
  return result;
}
(8).range()
// [0,1,2,3,4,5,6,7]

注意,数值的自定义方法,只能定义在它的原型对象 Number.prototype 上面,数值本身是无法自定义属性的。

var n = 1;
n.x = 1;
n.x // undefined

n 是一个原始类型的实例,直接在它上面新增一个属性 x,不会报错,但毫无作用,总是返回 undefined。这是因为一旦被调用属性,n就会转为 Number的实例对象,调用后,该对象自动被销毁。所以,下一次再次调用 n 的属性时,实际上是调用的另一个对象。

7 String 对象

7.1 概述

同样 String 可以作为构造函数,也可以作为类型转换函数。

var  s1 = "abc";
var s2 = new String("abc");
String(true) // "true"

typeof s1 // "string"
typeof s2 // "object"

s2.valueOf() // "abc"

字符串对象是一个类似数组的对象(很像数组,但不是数组)

new String("abc")
// String {0: "a", 1: "b", 2: "c", length: 3}
new String("abc")[0] // "a"

7.2 静态方法

7.2.1 String.fromCharCode()

7.3 实例属性

7.3.1 String.prototype.length

String 对象提供的静态方法(即定义在对象本身,而不是定义在对象实例的方法),主要是String.fromCharCode()。该方法的参数是一个或多个数值,代表 Unicode 码点,返回值是这些码点组成的字符串。

String.fromCharCode() // ""
String.fromCharCode(97) // "a"
String.fromCharCode(104, 101, 108, 108, 111)
// "hello"

如果String.fromCharCode 方法的参数为空,就返回空字符串;否则,返回参数对应的 Unicode 字符串。

String.fromCharCode(0x20BB7) === String.fromCharCode(0x0BB7) // true

String.fromCharCode 参数0x20BB7 大于 0xFFFF,导致返回结果出错。0x20BB7对应的字符是汉字,但是返回的确实另一个字符(码点0x0BB7

7.4 实例方法

7.4.1 String.prototype.charAt()

charAt 方法返回指定位置的字符,参数是从0 开始编号的位置。

var s = new String("abc");

s.charAt(1) // "b"

charAt 完全可以用数组下标替代。

"abc"[1] // "b"

注意,这个是索引的字符,所以中文也是一样的。

"你好吗?"[3] // "?"

7.4.2 String.prototype.charCodeAt()

charCodeAt 方法返回字符串指定位置的 Unicode 码点,相当于 String.fromCharCode 的逆操作。

"abc".charCodeAt(1) // 98

如果参数为负数,或大于等于字符串的长度,charCodeAt 返回 NaN

"abc".charCodeAt(-1) // NaN

7.4.3 String.prototype.concat()

concat 用于连接两个字符串,返回一个新字符串,不改变原字符串。

var s1 = "abc";
var s2 = "def";
s1.concat(s2); // "abcdef"
s1 // "abc"

该方法可以接受多个参数。

"a".concat("b", "c");  // "abc"

如果参数不是字符串,concat 方法会将其先转化为字符串,然后再连接。

var one = 1;
var two = 2;
var three = "3";
"".concat(one, two, three); // "123"
one + two + three // "33"

7.4.4 String.prototype.slice()

slice 方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是字符串的开始位置,第二个参数是子字符串的结束位置。

"JavaScript".slice(0,4) // "Java"

如果省略第二个参数,则表示子字符串一直到原字符串结束。

"JavaScript".slice(4) // "Script"

如果参数是负数,则表示从结尾开始倒数计算的位置,即该负值加上字符串的长度。

"JavaScript".slice(0,-6) // "Java"

如果第一次参数大于第二个参数,slice 方法返回一个空字符串。

"JavaScript".slice(2,1) // ""

7.4.5 String.prototype.substring()

substring 方法用于从原字符串取出子字符串并返回,不改变原字符串。这个函数的作用与slice类似,但是会自动把负的参数转化为正的参数,反直觉,不建议使用。

7.4.6 String.prototype.substr()

同上,不建议使用。

7.4.7 String.prototype.indexOf(), String.prototype.lastIndexOf()

indexOf就是用来检查字符串第一次出现的位置。如果返回-1,表示不匹配。

"hello world".indexOf("o") // 4

indexOf还可以接受第二个参数,表示向后匹配的开始位置

"hello world".indexOf("o", 5); // 7

lastIndexOf 的作用与IndexOf一致,只不过lastIndexof 匹配的方向正好相反。

"hello world".lastIndexOf("o") // 7

另外,lastIndexOf的第二个参数向前匹配的开始位置。

"hello world".lastIndexOf("o", 6) //4

7.4.8 String.prototype.trim

trim 能去除字符串两端的空格、制表符(\t/ \v),换行符\n和回车符\r

"\rhelloworld".trim()
// "helloworld"

7.4.9 String.prototype.toLowerCase(), String.prototype.toUpperCase()

toLowerCase() 是把字符串转为小写,toUpperCase则是全部转为大写。它们都返回一个新字符串,不改变原字符串。

"hello".toUpperCase() // "HELLO"
"hello".toUpperCase().toLowerCase() // "hello"

7.4.10 String.prototype.match()

match 方法用于确定字符串是哦副匹配某个子字符串,返回一个数组,成员为匹配的第一个字符串。如果没有找到匹配,则返回null

var matches = "catatat".match("at")
matches.index // 1
matches.input // catatat

match 方法可以使用正则表达式作为参数。

7.4.11 String.prototype.search(), String.prototype.replace()

search 方法的用法等同于 match, 但是返回值为匹配的第一个位置。如果没有找到匹配,则返回 -1

"cat,bat,sat".search("at") // 1

search 方法还可以使用正则表达式作为参数,详见《正则表达式》一节。
replace 方法用于替换匹配的子字符串,一般情况下只替换第一个匹配(除非使用带有g修饰符的正则表达式)

"aaa".replace("a", "b") // "baa"

7.4.12 String.prototype.split()

split 方法按照给定规则分割字符串,返回一个由分割出来的子字符串组成的数组。

"a|b|c".split("|") // ["a", "b", "c"]

如果连续有两个分割符,则返回数组之中会有一个空字符串。

"a||c".split("|") // ["a", "", "c"]

split 还可以接受第二个参数,限定返回数组的最大成员。

"a|b|c".split("|",2) // ["a", "b"]

split 方法还可以使用正则表达式作为参数

7.4.13 String.prototype.localeCompare()

localeCompare 方法用于比较两个字符串。它返回一个整数,表示第一个字符串小于第二个字符串;如果等于0,表示两者相等;如果大于0,表示第一个字符串大于第二个字符串。

"apple".localeCompare("banana") // -1
"apple".localeCompare("apple")

该方法的最大特点,就是会套率自然语言的顺序。举例来说,大写的英文字母小于小写字母。

"B" > "a" // false

但是,localeCompare 方法会考虑自然语言的排序情况,将B 排在a的前面。

"B".localeCompare("a") // 1

8 Math 对象

8.1 静态属性

Math 对象的静态方法,提供以下数学常数。

  • Math.E: 常数 e
  • Math.LN2: 2的自然对数
  • Math.LN10: 10的自然对数
  • Math.LOG2E: 以2为底的 e 的对数
  • Math.LOG10E: 以10为底的e的对数
  • Math.PI: 常数 π
  • Math.SQRT1_2: 0.5 的平方根
  • Math.SQRT2: 2 的平方根

8.2 静态方法

Math 对象提供以下一些静态方法。

  • Math.abs(): 绝对值
  • Math.ceil(): 向上取整
  • Math.floor(): 向下取整
  • Math.max(): 最大值
  • Math.min(): 最小值
  • Math.pow(): 指数运算
  • Math.sqrt(): 平方根
  • Math.log(): 自然对数
  • Math.exp(): e的指数
  • Math.round(): 四舍五入
  • Math.random(): 随机数

8.2.1 Math.abs()

Math.abs 方法返回参数值的绝对值。

Math.abs(-1) // 1

8.2.2 Math.max(), Math.min()

Math.max() 返回参数中的最大值,Math.min() 返回参数中的最小值。如果没有参数,Math.max() 返回-Infinity,Math.min() 返回-Infinity

Math.max(1,2,3) // 3
Math.min(2,3,4) // 2

8.2.3 Math.floor(), Math.ceil()

Math.floor 返回小于参数值的最大整数(地板值)

Math.floor(3.2) // 3
Math.floor(-3.2) // -4

Math.ceil 返回大于参数值的最小整数(天花板值)。

Math.ceil(3.2) // 4
Math.ceil(-3.2) // -3

这两个方法可以结合起来,实现一个总是返回数值的整数部分的函数。

function ToInteger(x){
  x = Number(x);
  return x < 0 ? Math.ceil(x) : Math.floor(x);
}

ToInteger(3.2) // 3
ToInteger(-3.2) // -3

8.2.4 Math.round()

Math.round方法用于四舍五入。

Math.round(0.1) // 0
Math.round(0.5) // 1

注意,对负数的处理,也会四舍五入往前进

Math.round(-0.5) // -0
Math.round(-0.6) // -1

8.2.5 Math.pow()

Math.pow() 方法返回第一个参数为底数,第二个参数为幂的指数值。

Math.pow(2,2) // 相当于 2 ** 2  // 4

8.2.6 Math.sqrt()

Math.sqrt 方法返回参数值的平方根,如果参数是一个负值,则返回 NaN

Math.sqrt(4) // 2
Math.sqrt(-4) // NaN

8.2.7 Math.log()

Math.log 方法以 e 为底的自然对数值。

8.2.8 Math.exp()

Math.exp 返回常数 e 的参数次方。

8.2.9 Math.random()

Math.random 返回 0 到 1之间的一个伪随机数,可能等于0,但一定小于1

Math.random()

8.2.10 三角函数方法

但愿我以后用不到,类绝不爱

9 Date 对象

Date 对象是 JS 原生的时间库,它以 1970年1月1日 00:00:00 作为时间的零点。

9.1 普通函数的用法

Date对象可以作为普通函数直接使用,返回一个代表当前时间的字符串。

Date()
// "Tue Aug 28 2018 16:42:03 GMT+0800 (香港标准时间)"

即使有参数,返回的也是当前时间。

9.2 构造函数的用法

Date还可以当作构造函数使用。对它使用new命令,会返回一个 Date对象。如果不加参数,实例代表的就是当前时间。

var today = new Date();
today
// "Tue Aug 28 2018 16:44:25 GMT+0800 (香港标准时间)"

作为构造函数,Date对象可以接受多种格式的参数,返回一个该参数对应的时间实例。

// 时间戳作为参数
new Date(1378218728000)
// Tue Sep 03 2013 22:32:08 GMT+0800 (香港标准时间)

// 参数为日期字符串
new Date("January 6, 2013")
// Sun Jan 06 2013 00:00:00 GMT+0800 (香港标准时间)

// 参数为多个整数,代表年、月、日、小时、分钟、秒、毫秒
new Date(2013, 0, )

第一点,参数可以是负整数,代表 1970 年元旦之前的时间。

new Date(-1378218728000)
//Fri Apr 30 1926 17:27:52 GMT+0800 (香港标准时间)

第二点,只要能被 Date.parse() 方法解析的字符串,都可以作为参数。

new Date("2018/8/28")
new Date("2018-8-28")

第三,参数为年、月、日等多个整数时,年和月是不能省略的,其他参数都可以省略的。也就是说,至少需要两个参数。

new Date(2012, 0)

最后各个参数的取值范围

  • 年,使用四位数年份,如果是负数表示公元前。如果是2位,表示19xx
  • 月,0表示1月,11表示12月
  • 日:1到31
  • 小时:1到23
  • 毫秒:0到999

参数如果超出了正常范围,会被自动折算。比如月设置为15,就折算为下年的4月。
参数可以使用负数,表示扣去的时间。

9.3 日期的运算

类型自动转化时,Date实例如果转为数值,则等于对应的毫秒数;如果转为字符串,则是对应的日期字符串。所以对两个日期实例进行减法运算,返回的是间隔的毫秒;进行加法运算,返回的是新字符串。

var d1 = new Date(2000, 2, 1);
var d2 = new Date(2001, 2, 1);
d1 - d2 // -31536000000

9.4 静态方法

9.4.1 Date.now()

Date.now 方法返回当前实际距离时间零点(1970年1月1日00:00:00)的毫秒数,相当于 Unix 时间戳(时间戳是秒数)乘以 1000。

Math.floor(Date.now() / 1000)  // 这样得出的就是时间戳了

9.4.2 Date.parse()

Date.parse() 用来解析日期字符串,返回该时间距离时间零点(1970年1月1日 00:00:00)
日期字符应该符合 YYYY-MM-DDTHH:mm:ss.sssZ格式,T是任意字符,Z 表示时区。其他格式的,也可以被解析:

Date.parse("2010-10-10")

如果解析失败,返回 NaN

Date.parse("2010-x")
// NaN

9.4.3 Date.UTC()

Date.UTC 接受年、月、日等变量作为参数,返回该时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数。

// 格式
Date.UTC(year, month, date, hrs, min, sec, ms)

// 用法
Date.UTC(2011, 0, 1, 2, 3, 4, 567)
// 1293847384567

9.5 实例方法

Date的实例方法,有几十个自己的方法,除了valueOftoString,可以分以下三类:

  • to类:从 Date 对象返回一个字符串,表示指定的时间
  • get类:获取 Date对象的日期和时间
  • set类:设置Date对象的日期和时间

9.5.1 Date.prototype.valueOf()

var d = new Date();
d.valueOf() // 1535459094337
d.getTime() // 1535459094337

预期为数值的场合,Date实例会自动调用该方法,所以可用下面的方法计算时间的间隔。

var start = new Date();
// ...
var end = new Date();
var elapsed = end - start;

9.5.2 to 类方法

(1) Date.prototype.toString()
toString 方法返回一个完整的日期字符串。

var d = new Date(2013, 0, 1);
d.toString() // "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"

(2) toUTCString 方法返回对应的 UTC 时间,也就是比北京时间晚 8 个小时。

var d = new Date(2013, 0, 1);
d.toUTCString() // "Mon, 31 Dec 2012 16:00:00 GMT"

(3) Date.prototype.toISOString()
toISOString 方法返回对应时间的 ISO8601 写法

var d = new Date(2013, 0, 1);
d.toISOString()
// "2012-12-31T16:00:00.000Z"

(4) Date.prototype.toJSON()
toJSON 方法返回一个符合 JSON 格式的 ISO 日期字符串,与 toISOString 方法返回的结果完全相同。

var d = new Date(2013, 0, 1);
d.toJSON()
// "2012-12-31T16:00:00.000Z"

(5) Date.prototype.toDateString()
toDateString 方法返回日期字符串(不含小时、分和秒)

var d = new Date(2013, 0, 1);
d.toDateString() // "Tue Jan 01 2013" 不含时分秒

(6) Date.prototype.toTimeString()
toTimeString 方法返回时间字符串(不含年月日)

var d = new Date(2013, 0, 1);
d.toTimeString() // "00:00:00 GMT+0800"

(7) 本地时间
以下三种方法,可以将 Date 实例转为表示本地时间的字符串。

  • Date.prototype.toLocaleString(): 完整的本地时间
  • Date.prototype.toLocalDateString(): 本地日期(不含小时、分和秒)
  • Date.prototype.toLocalTimeString(): 本地时间(不含年月日)

下面是用法实例。

var d = new Date(2013, 0, 1);
d.toLocaleString()
// "2013/1/1 上午12:00:00"

d.toLocaleDateString()
// "2013/1/1"

d.toLocaleTimeString()
// "上午 12:00:00"

这三个方法都有两个可选的参数。

dateObj.toLocaleString(locales, options)
dateObj.toLocaleDateString(locales, options)
dateObj.toLocaleTimeString(locales, options)

这两个参数中,locales 是指一个指定所用语言的字符串,options 是一个配置对象。下面是 locales 的例子。

var d = new Date(2013, 0, 1);
d.toLocaleString("en-US") // "1/1/2013, 12:00:00 AM"
d.toLocaleString("zh-CN") // "2013/1/1 上午12:00:00"

options 是一个配置项。

var d = new Date(2013, 0, 1);

// 时间格式
// 下面的设置是,星期和月份为完整文字,年份和日期为数字
d.toLocaleDateString("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
}) 
// "Tuesday, January 1, 2013"

// 指定时区
d.toLocaleTimeString("en-US",{
  timeZone: "UTC",
  timeZoneName: "short"
})
// "4:00:00 PM UTC"

d.toLocaleTimeString("en-US", {
  timeZone: "Asia/Shanghai",
  timeZoneName: "long"
})

// "12:00:00 AM China Standard Time"

// 小时周期为12还是24
d.toLocaleTimeString("en-US", {
  hour12: false
})
// "00:00:00"

9.5.3 get 类方法

Date对象提供了一系列get* 方法,用来获取实例对象某个方面的值。

  • getTime(): 返回实例距离 1970 年 1月 1日 00:00:00 的毫秒数,等同于 valueOf 方法。
  • getDate(): 返回实例对象对应每个月的几号(从 1 开始)
  • getDay(): 返回星期几,周日为0,周一为1,以此类推
  • getYear(): 返回距离 1900 的年份。
  • getFullYear(): 返回四位的年份
  • getMonth: 返回月份,0表示1月,11表示12月
  • getHours: 返回小时(0-23)
  • getMilliseconds: 返回毫秒(0-999)
  • getMinutes(): 返回分钟(0-59)
  • getSeconds: 返回秒
  • getTimezoneOffset():返回当前时间与 UTC 的时区差异,以分钟表示,
    所有这些 get* 方法返回的都是整数,不同的方法返回值的返回不一样。
  • 分钟和秒:0到59
  • 小时:0到23
  • 星期:0(星期天)到 6 (星期六)
  • 日期:1 到 31
  • 月份:0 (一月) 到 11 (十二月)
  • 年份:距离 1900 年的年数
var d = new Date("January 6, 2013");

d.getDate() // 6
d.getMonth() // 0
d.getYear() // 113
d.getFullYear() // 2013
d.getTimezoneOffset() // -480

下面一个例子,计算本年度还剩下多少天。

function leftDays(){
  var today = new Date();
  var endYear = new Date(today.getFullYear(), 11, 31, 23, 59, 999);
  var msPerDay = 24 * 60 * 60 * 1000;
  return Math.round((endYear.getTime() - today.getTime()) / msPerDay);
}

上面这些 get* 方法返回的都是当前时区的时间,Date对象还提供了这些方法对应的 UTC 版本,用来返回 UTC 版本,用来返回 UTC 时间。

  • getUTCDate()
  • getUTCFullYear()
  • getUTCMonth()
  • getUTCDay()
  • getUTCHours()
  • getUTCMinutes()
  • getUTCSeconds()
  • getUTCMillseconds()
var d = new Date("January 6, 2013");
d.getDate();
// 6
d.getUTCDate();
// 5

d 对象表示对于东八区来说是 1月6日0点0分0秒,这个时间对于 UTC 来说是 1月5日。

9.5.4 set 类方法

Date 对象提供了一系列 set* 方法,用来设置 实例对象的各个方面。

  • setDate(date): 设置实例对象对应的每个月的几号(1-31),返回改变后毫秒时间戳。
  • setYear(year): 设置距离 1900 年的年数。
  • setFullYear(year, month, date): 设置四位年份
  • setHours(hour, min, sec, ms): 设置小时
  • setMinutes(min, sec, ms): 设置分钟
  • setSeconds(sec, ms): 设置秒
  • setMilliseconds(ms): 设置毫秒
  • setTime(milliseconds): 设置毫秒时间戳。

这些方法基本上与 get* 一一对应,但是没有 setDay 方法,因为星期几是计算出来的,而不是设置的。另外需要注意,凡是涉及到计算月份的,都是从 0 开始算的,0 是 1月,11 是 12月。

var d = new Date("January 6, 2013");
d // "Sun Jan 06 2013 00:00:00 GMT+0800 (香港标准时间)"
d.setDate(9);
d // "Wed Jan 09 2013 00:00:00 GMT+0800 (香港标准时间)"

set* 方法的参数都会自动折算。以 setDate 为例,如果参数超过了当月最大的天数,则向下一个月顺延,如果参数是负数,表示从上个月的最后一天向前推算。

var d1 = new Date("January 6, 2013");
d1.setDate(32)
d1 // Fri Feb 01 2013 00:00:00 GMT+0800 (香港标准时间)
var d2 = new Date("January 6, 2013");
d2.setDate(-1);
Sun Dec 30 2012 00:00:00 GMT+0800 (香港标准时间)

set类方法和 get类方法,可以结合使用,得到相对时间。

var d = new Date();

// 将日期向后推 1000天
d.setDate(d.getDate() + 1000);
// 将时间设为 6 小时后
d.setHours(d.getHours() + 6);
// 将年份设置为去年
d.setFullYear(d.getFullYear() - 1);

set* 方法也有对应的 UTC 版本的,就不赘述了,反正也记不住。

10 RegExp 对象

10.1 概述

新建正则表达式有两种方法,一种是使用字面量,以斜杠表示开始和结束:

var regex = /xyz/;

另一种是使用 RegExp 构造函数。

var regex = new RegExp("xyz");

这两种写法是等价的,都新建了一个内容为 xyz 的正则表达式对象。它们的主要区别是,第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法是在运行时新建正则表达式,所以前者的效率较高。而且前者比较便利和直观,所以在实际应用中,基本都采用字面量定义正则表达式。

10.2 实例属性

正则对象的实例属性分为两类。
一类是修饰符相关,返回一个布尔值,表示对应的修饰符是否设置。

  • RegExp.prototype.ignoreCase: 返回一个布尔值,表示是否设置了 i 修饰符
  • RegExp.prototype.global: 返回一个布尔值,表示是否设置了 g 修饰符。
  • RegExp.prototype.multiline: 返回一个布尔值,表示是否设置了 m 修饰符。
    上面三个属性都是只读属性。
var r = /abc/igm;
r.ignoreCase // true
r.global // true
r.multiline // true

另一类与修饰符无关的属性,主要是下面两个。

  • RegExp.prototype.lastIndex: 返回一个整数,表示下一次开始搜索的位置。该属性可读写,但是只在进行连续搜索时有意义。
  • RegExp.prototype.source: 表示正则表达式的字符串形式(不包括反斜杠,也不包括标识符),该属性只读。
var r = /abc/igm;
r.lastIndex // 0
r.source // "abc"

10.3 实例方法

10.3.1 RegExp.prototype.test()

正则实例对象的 test 方法返回一个布尔值,表示当前模式是否能匹配参数字符串。

/cat/.test("cats and dogs") // true

如果正则表达式带有 g 修饰符,则每一次 test 方法都从上一次结束的位置开始向后匹配的。

var r = /x/g;
var s = "_x_x_x";

r.lastIndex // 0
r.test(s) // true

r.lastIndex // 2
r.test(s) // true
r.lastIndex // 4

上面的代码的正则表达式使用了 g 修饰符,表示全局搜索,会有多个结果。三次使用 test,每一次开始搜索的位置都是上一次匹配的后一个位置。

带着 g 修饰符时,可以通过正则表达式的 lastIndex 属性指定开始搜索的位置。

var r = /x/g;
var s = "_x_x";

r.lastIndex = 4;
r.test(s) // false

注意,带有 g 修饰符时,正则表达式内部会记住上一次的 lastIndex 属性,这时不应该更换所要匹配的字符串,否则会有一些难以察觉的错误。

var s = /bb/g;

s.test("bb"); // true
s.test("-bb-") // false

死循环的那个我有点不明白阮大的思想。

如果正则模式是一个空字符串,则匹配所有字符串。

new RegExp("").test("abc") 
// true

10.3.2 RegExp.prototype.exec()

正则实例对象的 exec 方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回null

var s = "_x_x";
var r1 = /x/g;
var r2 = /y/;

r1.exec(s) // ["x"] 
r2.exec(s) // null

// 我对g有一个误解,认为用上g就会把所有的匹配结果返回,看来不是的。正则匹配成功后就会返回,这样也合乎逻辑,高效。
上面代码中,正则对象 r1 匹配成功,返回一个数组,成员是匹配结果;匹配失败就返回 null.

如果正则表达式包含圆括号(即组匹配),那么返回的数组会包含多个成员。第一个成员是整个匹配的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,依此类推。整个数组的 length 属性等于组匹配的数量再加1.

var s = "_x_x";
var r = /_(x)/;

r.exec(s) // ["_x", "x"]

上面代码的exec 方法,返回一个数组,第一个成员是整个匹配的结果,第二个成员是圆括号匹配的结果。

exec 方法返回数组还包含以下两个属性:

  • input:整个原字符串
  • index: 整个匹配成功的开始位置(从0开始计数)
var r = /a(b+)a/;
var arr = r.exec("_abbba_aba_");

arr // ["abbba", "bbb"]
arr.index // 1
arr.input // "_abbba_aba_"

如果正则表达式加上 g修饰符,则可以多次使用exec 方法,下一次搜索的位置从上一次匹配成功结束的位置开始。

var reg = /a/g;
var str = "abc_abc_abc";

var r1 = reg.exec(str);
r1 // ["a"]
r1.index //0
r1.lastIndex // 1

var r2 = reg.exec(str);
r2 // ["a"]
r2.index // 4
reg.lastIndex // 5

var r3 = reg.exec(str);
r3 // ["a"]
r3.index // 6
reg.lastIndex // 7
var r4 = reg.exec(str);
r4 // ["a"]
r4.index // 8
r4.lastIndex // 9

10.4 字符串的实例方法

字符串的实例方法之中,有4种与正则表达式有关。

  • String.prototype.match(): 返回一个数组,成员是所有匹配的子字符串。
  • String.prototype.search(): 按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置
  • String.prototype.replace(): 按照给定的正则表达式进行替换,返回替换后的字符串
  • String.prototype.split(): 按照给定的规则进行字符串分割,返回一个数组,包含分割后的各个成员。

10.4.1 String.prototype.match()

字符串实例对象的 match 方法对字符串进行正则匹配,返回匹配结果。
// 哦,test 和 exec 是正则表达式对象的方法,而这四个是实例的方法。

var s = "_x_x";
var r1 = /x/;
var r2 = /y/;

s.match(r1) // ["x"]
s.match(r2) // null

从上面代码可以看出,字符串 match 方法与正则对象的 exec 方法非常类似:匹配成功返回一个数组,匹配失败返回 null

如果正则表达式带有 g 修饰符,则该方法与正则对象的 exec 方法行为不同,会一次性返回所有匹配成功的结果。

var s = "abba";
var r = /a/g;

s.match(r) // ["a","a"]
r.exec(s) // ["a"]

设置正则表达式的 lastIndex 属性,对match方法无效,匹配总是从字符串的第一个字符开始。(这也正常,毕竟这是 String 的方法)

var r = /a|b/g;
r.lastIndex = 7;
“xaxb”.match(r) 
r.lastIndex // 0

10.4.2 String.prototype.search()

字符串对象的 search 方法,返回第一个满足条件的匹配结果在整个字符串的位置。如果没有任何匹配,则返回 -1.

"_x_x".search(/x/)
// 1

10.4.3 String.prototype.replace()

字符串对象的 replace 方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。

str.replace(search, replacement)

正则表达式如果不加 g 修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。

"aaa".replace("a", "b"); // "baa"
"aaa".replace(/a/, "b"); // "baa"
"aaa".replace(/a/g, "b"); // "bbb"

上面代码中,最后一个正则表达式使用了g修饰符,导致所有的b都被替换掉了。

replace 方法的一个应用,就是消除字符串首位两端的空格。(其实,直接使用 trim 就好了)

var str = " #id div.class ";
str.replace(/^\s+|\s+$/g, "") // 这个正则表达式的意思是,以空格开头或以空格结尾。
// "#id div.class"

replace 方法的第二个参数可以使用美元符号$,用来指代所替换的内容。

  • $&: 匹配的子字符串
  • $`: 匹配结果前面的文本
  • $': 匹配结果后面的文本
  • $n: 匹配成功的第 n 组内容,n 是从1开始的自然数
  • : 指的是美元符号$
"hello world".replace(/(\w+)\s(\w+)/, "$2 $1")
// "world hello"

第二个例子估计是阮老师写错了

上面代码中,第一个例子是将匹配的组互换位置。
replace 方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值。

"3 and 5".replace(/[0-9]+/g, function(match){
  return 2 * match;
})

作为 replace 方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。下面是一个网页模板替换的例子。

var prices = {
  p1 : "$1.99",
  p2 : "$9,99",
  p3 : "$5.00"
}

var template = `<span id="p1"></span>
<span id="p2"></span>
<span id="p3"></span>

template.replace(/(<span id=")(.*?)(">)(<\/span>)/g, function(match, $1, $2, $3, $4){
  return $1 + $2 + $3 + prices[$2] + $4;
})

四个括号,会产生四个组匹配,在匹配函数中用 $1$4 表示。匹配函数的作用是将价格插入到模板中。

10.4.4 String.prototype.split()

字符串对象的split 方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。

str.split(separator, [limit])

该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数。

// 非正则分隔
"a,  b,c, d".split(",")
// ["a"," b", "c", "d"]

// 正则分割,去除多余空格
阮神写的这个有点奇怪,我更喜欢这么写
s.split(/,\s*/)

// 指定返回数组的最大成员
"a,  b,c, d".split(/,\s*/, 2)
["a", "b"]

10.5 匹配规则

10.5.1 字面量字符和元字符

如果在正则表达式之中,某个字符只是表示它的字面含义,这种就叫做 “字面量字符”。
还有一部分字符有特殊含义,不代表字面的意思,它们叫做“元字符”。
(1) 点字符(.)
点字符不能匹配换行符\n, 回车 \r, 行分隔符\u2028。除此之外,所有的字符,都能匹配。
(2)位置字符
位置字符表示字符所处的位置,主要有两个字符。

  • ^ 表示字符串的开始位置
  • $ 表示字符串的结束位置
// test 必须出现在开始位置
/^test/.test("test123") // true

// test 必须出现在结束位置
/test$/.test("new test") // true

// 从开始位置到结束位置只有 test
/^test$/.test("test") // true
/^test$/.test("test test") // true 

(3) 选择符(|
竖线符号| 在正则表达式中表示"或关系"(OR),即 cat|dog 表示匹配 catdog

/11|22/.test("911") // true

可以使用括号来避免歧义。

// 写一个匹配数字或字母的表达式,并至少存在1个
(\w|\d)+
// \w|\d+ 如果这么写,意思就变了。

10.5.2 转义符

反斜杠\ 是转义符,表示匹配元字符串本身。
需要转义的元字符有 12 个:

^, 元字符表示开始
$, 元字符表示结束
. , 表示匹配除换行符、回车等特殊字符之外的所有字符
[, 可以把前面字符重复的次数写在里面
(), 可以表示分组
|, 表示或
+,表示至少匹配1次
*,表示至少匹配0次
?, 表示吝啬模式,尽可能少的匹配
\, 表示转义
{,这个不知道

阮神对/ 也进行转义了,即写成了\/。要是这么算的话,就 13个。
提示,如果使用 RegExp 方法生成正则表达式,转义需要使用两个斜杠,因为字符串内部会先转义一次。

(new RegExp('1\+1')).test('1+1') // false

(new RegExp("1\\+1")).test("1+1") // true

使用// 模式,就不必这样。

/1\+1/.test("1+1") // true

10.5.3 特殊字符

正则表达式对于一些不能打印的特殊字符,提供了表达方法。

  • \cX 表示 Ctrl - [X],其中 x 是 A -Z 的任何一个字母,用来匹配控制字符
  • [\b] 匹配退格键
  • \n, 匹配换行符
  • \r , 匹配回车符
  • \t, 表示制表符
  • \v,表示垂直制表符
  • \f, 表示换页符
  • \0, 表示 null 字符
  • \xhh,匹配一个以两位十六进制数(\x00-\xFF) 表示的字符
  • \uhhhh, 匹配一个以四位十六进制数(\u000-\uFFFF) 表示的 Unicode 字符。

10.5.4 字符类

字符类表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如 [xyz] 表示 x, y, z 之中任选一个匹配。
---- 这个以前不知道啊!

/[abc]/.test("hello world") // false
/[abc]/.test("apple") // true

有两个字符在字符串中有特殊含义。
(1)脱字符(^)
如果方括号内第一个字符是[^],则表示除了字符类之中的字符,其他字符都是可以匹配的。[^xyz] 表示除了 x / y / z, 其他字符都是可以匹配的

/[^abc]/.test("hello world") // true
/[^abc]/.test("abc") // false

如果方括号中没有其他字符串,只有 [^],表示匹配一切字符串,其中包括换行符、制表符等。

var str = "abc\nw";
/abc[^]w/.test(str) // true
/abc.w/.test(str) // false

注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义。

/[a^b]/.test("a^b") // true

(2) 连字符(-)
某些情况下,对于连续序列的字符,连字符(-) 用来提供简写形式,表示字符的连续范围。比如,[abc]可以写成[a-c][0123456789] 可以写成 [0-9],同理[A-Z] 表示26个大写字母。

/a-z/.test("b") // false
/[a-z]/.test("b") // true

以下都是合法的字符类简写形式

/[0-9.,]/
/[0-9a-fA-F]/
/[a-zA-Z0-9-]/
/[1-31]/

最后一个字符类 [1-31],不代表 1 至 31,只代表 1 至 3。因为这是1位的匹配,最多到9.
连字符还可以用来指定 Unicode 字符的范围。

var str = "\u0130\u0131\u0132";
/[\u0128-\uFFFF]/.test(str)
// true

不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的就是 [A - z],表面上它是选中从大写的 A 到小写的 z 之间 52 个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果。

/[A-z]/.test("\\") // true

10.5.5 预定义模式

预定义模式指的是某些常见模式的简写方法。

  • \d 匹配 0-9 之间的任一数字,相当于 [0-9]
  • \D 匹配所有 0-9 之外的字符,相当于 [^0-9]. —— 这个忘了
  • \w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9]。注意,之前我总是要加一个| 或符号。
  • \s 匹配空格(包括换行符、制表符、空格符等),相当于 [\t\r\n\v\f]
  • \S 匹配非空格字符,相当于 [^\t\r\n\v\f]
  • \b 匹配词的边界。—— 这个之前不知道啊
  • \B 匹配非词的边界,即在词的内部。
// \s 的例子
/\s\w*/.exec("hello world") // [" world"]

// \b 的例子
/\bworld/.test("hello world") // true
/\bworld/.test("hello-world") // true
/\bworld/.test("helloworld") // false

// \B 的例子
/\Bworld/.test("hello-world") // false
/\Bworld/.test("helloworld") // true

\b 表示词的边界,所以 world 的词首必须独立(词尾是否独立未指定),才会匹配。同理,\B 表示非词的边界,只有world 的词首不独立,才会匹配。

通常,正则表达式遇到换行符(\n)就会停止匹配。

var html = "<b>Hello</b>\n<i>world</i>"

/.*/.exec(html)[0] // <b>Hello</b>
/[\S\s]*/.exec(html)[0] "<b>Hello</b>\n<i>world</i>"

[\S\s] 指定一切字符。

10.5.6 重复类

模式的精确匹配次数,使用大括号 {} 表示。{n} 表示恰好重复n次,{n, } 表示至少重复n 次,{n,m} 表示重复不少于 n 次,不多于m次。

/lo{2}k/.test("look") // true
/lo{2,5}k/.test("lok") // false
/lo{2,5}k/.test("loooook") // true
/lo{2,5}k/.test("looooook") // false

10.5.7 量词符

量词符用来设定某个模式出现的次数。

  • ? 问号用来表示某个模式出现0次或1次,等于 {0,1}
  • * 星号表示某个模式出现0次或多次,等同于 {0,}
  • + 加号表示某个模式出现1次或多次,等同于 {1, }
// t 出现0次或1次
/t?est/.test("test") // true
/t?est/.test("est") // true

// t 出现1次或多次
/t+est/.test("test") // true
/t+est/.test("ttest") // true
/t+est/.test("est") // false

// t 出现 0 次或多次
/t*est/.test("test") // true
/t*est/.test("ttest") // true
/t*est/.test("est") // true

10.5.8 贪婪模式

? / + / * 这三个量词,默认情况下都是最大可能的匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式。

/a+/.exec("aaa") // true

如果想改为非贪婪模式,可以在量词符后面加一个问号。

var s = "aaa";
/a+?/.exec(s); // [a]

非贪婪模式:

  • *?: 表示某个模式出现 0 次或多次,匹配时采用非贪婪模式。
  • +?: 表示某个模式出现 1 次或多次,匹配时采用非贪婪模式。

10.5.9 修饰符

修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。
修饰符可以单个使用,也可以多个一起使用。

// 单个修饰符
var regex = /test/i;

// 多个修饰符
var regex = /test/ig;

(1) g 修饰符
默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。g 修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。

var regex = /b/;
var str = "abba";

regex.test(str); // true
regex.test(str); // true
regex.test(str); // true

只要含 g 修饰符,每次都是从上一次匹配成功处,开始向后匹配。

var regex = /b/g;
var str = "abba";

regex.test(str) // true
regex.test(str) // true
regex.test(str) // false

(2) i 修饰符
默认情况下,正则表达式区分大小写。

/a/.test("A") // false

i (ignorecase) 表示忽略大小写。

/abc/i.test("aBc") // true

(3) m 修饰符
m 修饰符表示多行模式(multiline),会修改^$的行为。默认情况下(即不加m修饰符),^$ 匹配字符串的开始处和结尾处,加上m修饰符以后,^$还会匹配行首和行尾,即^$会识别换行符(\n)。

/world$/.test("hello world\n") // false
/world$/m.test("hello world\n") // true

加上m后,$可以匹配行尾。

10.5.10 组匹配

(1)概述
正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。

// 阮大举的例子,我曾经也困惑过。
/fred+/.test("fredd") // true
/(fred)+/.test("fredfred") // true

上面的代码中,第一个模式没有括号,结果+ 只表示重复字母d,第二个模式有括号,结果+就表示重复fred这个词。

下面是另外一个分组捕获的例子。

var m = "abcabc".match(/(.)b(.)/);
m // ["abc", "a", "c"]
这个特性也是非常蛋疼的 ---> 使用组匹配时,不宜同时使用g修饰符,否则 match 方法不会捕获分组的内容。
var m = "abcabc".match(/(.)b(.)/g);
m 
// ["abc", "abc"];

match 方法没有显示出分组内容。这时,必须使用正则表达式的exec方法,配合循环,才能读到每一轮匹配的组捕获。

var str = "abcabc";
var reg = /(.)b(.)/g;
while(true){
  var result = reg.exec(str);
  if (!result){
    break;
  }
}
// ["abc", "a", "c"]
// ["abc", "a", "c"]

正则表达式内部,还可以使用 \n 引用括号匹配的内容,n是从1 开始的自然数,表示对应顺序的括号。

/(.)b(.)\1b\2/.test("abcabc") // true

括号还可以嵌套。

// 这个脑子还是转了一下才看懂
/y((..)\2)\1/.test("yabababab") 
// true

组匹配非常有用,下面是一个匹配网页标签的例子。

// 我曾写过类似用途的正则表达式,但是我想的不周全。没有考虑到`^<` 这种表示方法。
var tagName = /<([^>]+)>[^<]*<\/\1>/;
tagName.exec("<b>bold</b>") // ["<b>bold</b>", "b"]

(2) 非捕获组
(?:x) 称为非捕获组,表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。

非捕获分组的作用请考虑这样一个场景,假定需要匹配 foofoofoo,正则表达式就应该写成 /(foo){1,2}/, 但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为/(?:foo){1,2}/, 它的作用与前面是一样的,但是不会单独输出括号内部的内容。

受此启发,写一个正则表达式,输出标签中的内容。(你永远想不出你不知道的解决办法,只有学的越多,解决办法才能越多越灵活)

var reg = /<(?:[^>]+)>([^<]*?)<\/(?:[^>]+)>/
reg.exec("<a><b>this is a good</b></a>")
// [<b>this is a good</b>", "this is a good"]

常用来分解网址的正则表达式。

// 哦,这么写表达式,不对 / 转义就没法写了。
var url = /(http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*?)/;

url.exec("http://www.baidu.com/")
// ["http://www.baidu.com/", "http", "www.baidu.com", "/"]

// 非捕获分组
var url = /(?:http|ftp):\/\/([^\/\r\n]+)(\/[^\r\n]*)/;
url.exec("http://www.baidu.com/")
// ["http://www.baidu.com/", "www.baidu.com", "/"]

(3) 先行断言
x(?=y) 称为先行断言,x 只有在 y前面才匹配,y 不会被计入返回结果。比如,要匹配后面跟着百分号的数字,写成/\d+(?=%)/

/\d+(?=%)/.exec("52%") // ["52"]

“先行断言” 中,括号中的内容是不会返回的。

/b(?=c)/.exec("abc") // ["b"]

(4) 先行否定断言
x(?=y) 先行断言是只有y存在时才匹配x且y不出现在返回结果中,
x(?!y) 先行否定断言是只有y不存在时才匹配x且y不出现在返回结果中。
比如要匹配非百分号前面的数字。

/\d+(?!%)/.exec("23") // ["23"]

/\d+(?!%)/.exec("23%") // ["2"]

11 JSON 对象

11.1 JSON 格式

每个JSON 对象必须是一个值,不能是两个值。这个值可能是一个值,可能是一个数组或对象。

JSON 对于值的类型和格式有严格规定。

  1. 复合类型的值必须是数组或对象,不能是函数、正则表达式对象、日期对象。
  1. 原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和 null。(不能使用NaN / Infinity / -Infinity / undefined
  2. 字符串必须使用双引号,不能使用单引号
  3. 数组或对象最后一个成员后面不能加逗号。(这点在JS或PY中都是合法的)

11.2 JSON 对象

11.3 JSON.stringify()

11.3.1 基本用法

JSON.stringify 方法用于将一个值转为 JSON 字符串。这个值可以被 JSON.parse 方法还原。

JSON.stringify("abc") // ""abc""
JSON.stringify(false) // "false"
JSON.stringify(1) // "1"
JSON.stringify({}) // "[]"

JSON.stringify([1, "false", false])
// "[1, "false", false]"

注意,对原始类型的字符串转为JSON后会带双引号。

JSON.stringify("abc") === "\"abc\"" // true

如果对象的属性是undefined、函数或 XML 对象,该属性会被 JSON.stringify 过滤。

JSON.stringify({
  a : undefined,
  b : ()=>{},
})
// "{}"

如果数组的成员是undefined / 函数或xml对象,则会被转为 null。

JSON.stringify([undefined, ()=>{}])  // "[null, null]"

正则对象会被转为空。

JSON.stringify(/a/) // "{}"

JSON.stringify 会忽略不可遍历的属性。

var obj = {};
Object.defineProperties(obj, {
  "foo": {
    value : 1,
    enumerable: true,
  }, 
  "bar": {
    value: 1,
    enumerable: false
  }
})
JSON.stringify(obj) // "{foo: 1}"

11.3.2 第二个参数

JSON.stringify 还可以接受一个数组作为第二个参数。
第二个参数可以是一个数组,数组只对对象的属性有效,对数组无效。这个数组相当于白名单,指定接受 stringify 的范围。

JSON.stringify({a:1,b:2,c:3},[a,c])
// "{"a":1, "c":3}"

第二个参数还可以是一个函数,用来更改JSON.stringify 的返回值。这个函数分别接受两个参数,分别是对象的键名和键值。

JSON.stringify({a:1,b:2,c:3}, (key, value)=>{
  if(value % 2 === 0){
    value = value * 2;
  }
  return value;
})
// "{"a":1,"b":4,"c":3}"

这个函数是递归处理所有的键。

var o = {a: {b: 1}};

function f(key, value){
  if (typeof(value) === "number"){
    value = 222;
  }
  return value;
}

JSON.stringify(o, f)
// '{"a": {"b": 1}}'

如果处理函数返回 undefined 或没有返回值,则该属性会被忽略。

function f(key, value){
  if(typeof(value) === "string"){
      return undefined;
  }
  return value;
}

JSON.stringify({a: "abc", b: 123}, f)
// '{"b": 123}'

11.3.3 第三个参数

JSON.stringify 还可以接受第三个参数,用于增加返回的 JSON 字符串的可读性。如果是数字,表示每个属性前面添加的空格(最多不超过10个);如果是字符串,则该字符串会添加在每行前面。

JSON.stringify({p1: 1, p2: 2}, null, 2);
"{
    "p1":  1,
    "p2":  2
}"

JSON.stringify({p1: 1, p2: 2}, null, "|-");

"{
|-"p1": 1,
|-"p2":  2
}"

11.3.4 参数对象的 toJSON 方法

如果参数对象有自定义的 toJSON 方法,那么 JSON.stringify 会使用这个方法的返回值作为参数,而忽略原对象的其他属性。

var user = {
  firstName : "三",
  lastName : "张", 

  get fullName(){
      return this.lastName + this.firstName;
  }
};

JSON.stringify(user)
// "{"firstName":"三", "lastName":"张", "fullName":"张三"}"

现在,为这个对象加上toJSON 方法。

var user = {
  firstName : "三",
  lastName : "张", 

  get fullName(){
      return this.lastName + this.firstName;
  }

  toJSON: function(){
      return {
        name: this.lastName + this.firstName
      }
  }
};

JSON.stringify(user); // "{"name": "张三"}"

Date 对象就有一个自己的 toJSON 方法。

var date = new Date("2015-01-01");
date.toJSON(); // "2015-01-01T00:00:00.000Z"
JSON.stringify(date); // "2015-01-01T00:00:00.000Z"

toJSON 方法的一个应用是,将正则对象自动转化为字符串。因为 JSON.stringify 默认不能转换正则对象,但是设置了toJSON 方法以后,就可以转化正则对象了。

var obj = {
  reg: /foo/
};

// 不设置 toJSON 方法时
JSON.stringify(obj) // "{"reg": {}}"

// 设置 toJSON 方法
RegExp.prototype.toJSON = RegExp.prototype.toString;
JSON.stringify(obj) // "{"reg": "/foo/"}"

上面代码在正则对象的原型上部署了 toJSON 方法,将其指向toString 方法,因此遇到 JSON时,正则对象就先调用 toJSON 转为字符串,然后再被JSON.stringify 方法处理。

11.4 JSON.parse()

JSON.parse 方法将 JSON 字符串转为对应的值。

JSON.parse("{}") // {}
JSON.parse('true') // true
JSON.parse('"foo"') // "foo"
JSON.parse("null") // null
JSON.parse('[1, 5, "false"]') // [1, 5, "false"]
JSON.parse('{"name": "张三"}')  // {name: "张三"}

如果传入的字符串不是有效的 JSON 格式,JSON.parse 方法将报错。

JSON.parse(" 'String' ") // SyntaxError: Unexpected token ' in JSON

JSON.parse方法可以接受一个处理函数,作为第二个参数,用法与JSON.stringify 方法类似。

function f(key, value){
  if (key === "a"){
    return value + 10;
  }
  return value;
}
JSON.parse('{"a": 1, "b": 2}');
// {a: 11, b: 2}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,812评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,626评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,144评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,052评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,925评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,035评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,461评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,150评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,413评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,501评论 2 307
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,277评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,159评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,528评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,868评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,143评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,407评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,615评论 2 335

推荐阅读更多精彩内容