Back to prev

Javascript INFO notes

Jan 5, 2022
Linkang Chan
@Jesse Chan

本文记录现代 Javascript 的一些学习笔记,从头到尾的全面学习下 javascript。 从文档上看,这个教程的质量和细节部分还是挺多的。记录的内容大多是我没有接触过,或者比较模糊的部分。

空值合并运算符

??是用于在值为null/undefined这两个情况下返回第一个已经定义的值。 这个比||行为不同的是:它对0/""等这些情况会认为是已定义的。??的优先级非常低,需要注意使用括号。

> let firstName = null
undefined
> let secondname = ''
undefined
> let c = secondname ?? 'me?'
undefined
> c
''
> let d = firstName ?? 'me!'
undefined
> d
'me!'
>

同时可以通过多个串起来使用:

> let d = firstName ?? 'me!'
undefined
> d
'me!'
> let e=null
undefined
> let g = e ?? firstName ?? secondname ?? d
undefined
> g
''
>

这边就会返回secondname这个非空的值。

函数

  • 不要在return与返回值之间添加新行,这会导致 return 变成单行指令,返回空置;
// bad case
return   // will be treat as return;
  (a + b)

// optimized
return (
    a + b
)
  • 函数表达式后尽量增加一个;
let add = function (a, b) {
    return a + b;
};

函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。

代码质量

  • debugger;单独的语句会让调试器在此暂停住,并且不会执行下一条语句。
  • 当处于调试模式时,在代码中的某一行上右键,在显示的关联菜单(context menu)中点击一个非常有用的名为 “Continue to here” 的选项。

[devTool doc]: Chrome dev tool 的相关文档

文档注释

[JsDoc]: 用于注释 js 函数的一个中格式,搭配工具可以生成 html 的文档
[JsDoc 3]: 用于自动生成 js 注释文档的工具

语言规范

[ES6]: 查看语言特性的当前支持状态

Object

  • 多单词的 key 需要使用引号包裹
let obj = {
    a: 1,
    b: 2,
    "hello world": 3,
};

console.log(obj['hello world']) // 3
key='hello world'
console.log(obj[key]) // 3
console.log(obj.key) // undefined
  • 当数字 0 被用作对象的属性的键时,会被转换为字符串 “0”
  • 整数属性会被排序,其他的属性会按照创建的顺序排序
let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};

for(let code in codes) {
  alert(code); // 1, 41, 44, 49
}
  • 使用 const 声明的对象也是可以被修改的

  • Object.assign 来做所谓的“浅拷贝”, 因为嵌套的对象同样存在引用

> const abc = {'a': 1, 'c': {'d': 2}}
> abc
{ a: 1, c: { d: 2 } }
> abcd = Object.assign({}, abc)
{ a: 1, c: { d: 2 } }
> abcd.c.d=4
4
> abc
{ a: 1, c: { d: 4 } }
>

构造函数

常规来说,它们也是普通的函数,不过有如下的规定:

  • 它们的命名以大写字母开头。
  • 它们只能由 “new” 操作符来执行。

JS 中形如Date或者Set等等的构造函数,都需要使用new操作符来创建。 通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。但是,如果这有一个 return 语句,那么规则就简单了:

+ 如果 return 返回的是一个对象,则返回这个对象,而不是 this。
+ 如果 return 返回的是一个原始类型,则忽略。

一个例子:

function Person(name) {
    this.name = name;
    return {"newname" : this.name }
}

p = new Person('John')
console.log(p) // { newname: 'John' }

// normal variable
function Person2(name) {
    this.name = name;
    let age = 18;
    return age;
}

p2 = new Person2('John')
console.log(p2) // { name: 'John' }

可选链

?.这个一般用在判断一个 object 的属性是否存在,如果存在,则返回这个属性的值,否则返回 undefined。 所谓的存在一般指的是这个属性是非 null/undefined的。

ret = a?.b?.c?.d

这可以省去大部分的重复判断代码。除此之外还有?.()?.[]这两个变体。 这些是可以用于删除和读取的,但是不能用于写入。

let userAdmin = {
  admin() {
    alert("I am admin");
  }
};
let userGuest = {};
userAdmin.admin?.(); // I am admin
userGuest.admin?.(); // 啥都没有(没有这样的方法)

// 可选 key

let user = null

user?.['helloworld'] // undefined
delete user?.key // 此处不存在key,所以不会删除,但不会报错

注意以上再不存在时均返回undefined

Symbol

Symbol表示唯一的标识符。可以用于在 object 中,作为 object 的属性名,但是它不会被转换成字符串,是属于独立的类型。 如果想要获取他的定义或者是值,使用如下:

let id = Symbol("id")
console.log(id.toString()) // Symbol(id)
console.log(id.description) // id

symbol 的隐藏属性可以让我们安全的在第三方的结构里面增加新的key。 比如:

let id = Symbol("id")
let user = {
    name: "John",
}

user[id] = 123
console.log(usr[id]) // 123
  • for...in循环中,Symbol 属性会被忽略。
  • Object.keys()中,Symbol 属性会被忽略。
  • Object.assing()中,Symbol 属性会被复制。

全局 symbol

  • 使用Symbol.for("xxx")来获取全局 symbol。 如果不存在就创建一个新的。存在的就直接返回。
  • 使用Symbol.keyFor("xxx")来获取 symbol 的名字

从技术上说,Symbol 不是 100% 隐藏的。有一个内建方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 Symbol。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 Symbol。所以它们并不是真正的隐藏。但是大多数库、内建方法和语法结构都没有使用这些方法。

primitive

可以使用Symbol.primitive的内建 symbol 用来转换方法命名,这个类似于将 object 变成可以处理不同场景下的情况。

let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

alert(user); // hint: string -> {name: "John"}
console.log(+user); // hint: number -> 1000
console.log(user + 500); // hint: default -> 1500

默认情况下,普通对象具有 toString 和 valueOf 方法:

  • toString 方法返回一个字符串 “[object Object]”。
  • valueOf 方法返回对象自身。
let user = {name: "john"}
console.log(user.valueOf()) // {name: "john"}

此时我们可以重写toStringvalueOf方法

let user = {
  name: "john",
  value: 500,
  toString() {
    return `{name: "${this.name}"}`
  },
  valueOf() {
    return this.value
  }
}

alert(user)  // {name: "John"}
console.log(user + 500) // 1000

数字类型

toString(base)返回当前数字以 base 形式的字符串。

> let number = 100
100
> number.toString(2)
'1100100'
> number.toString(16)
'64'
>

同样的当我们将字符串的数字变成数字时,parseIntparseFloat可以帮助我们。不过这parseInt拥有第二个参数,用来制定进制。

> let anumber = '100'
> parseInt(anumber) // 100
> parseInt(anumber, 2) // 4, 将 100 看成是二进制的

String

字符串的indexOf的返回值可以使用一个按位与的小技巧来判断。如下:

let str = "hello world";
if(~str.indexOf("o")) {
  console.log("find it")
}

~就是按位取反, 所以~n等价于-(n+1)。 也就是说~-1= 0。 0 在 js 中是表示 falsy 值的。不过这个技巧不太建议使用,因为有一定的局限性,且我们可以使用includes来替代。

一些常用的方法的总结:

| 方法 | 选择方式 | 负数参数 |
| ---- | ---- | ---- |
| slice(start, end)	| 从 start 到 end(不含 end)| 	允许 |
| substring(start, end)	| start 与 end 之间(包括 start,但不包括 end)| 	负值代表 0 |
| substr(start, length)	| 从 start 开始获取长为 length 的字符串	| 允许 start 为负数 | 

array

循环遍历数组可以有如下两种方式:

  • for (let i=0; i<arr.length; i++)— 运行得最快,可兼容旧版本浏览器。
  • for (let item of arr)— 现代语法,只能访问 items。
  • for (let i in arr)— 永远不要用这个。

数组默认有一个toString的操作。所以我们可以如下操作:

> let a = [1,2,3,4]
> String(a)
'1,2,3,4'

splice(start[, deleteCount, elem1, …, elemN])

  • 第一个参数表示start的位置;可以是负数,也可以是 0 等等;
  • 第二个参数表示要删除多少个元素,可以是 0;
  • 往后的参数表示需要插入的元素,从 start 的位置之后开始。

concat

连接多个数组,如果一个 object 是可以被看作数组时,也是可以使用:

> let arr = [1,2]
> let obj = {
... "index": 1,
... "array": 2,
... [Symbol.isConcatSpreadable]: true,
... length: 2 }
> arr.concat(obj)
[ 1, 2, <2 empty items> ]
> let arrayLike = {
...   0: "something",
...   1: "else",
...   [Symbol.isConcatSpreadable]: true,
...   length: 2
... };
> arr.concat(arrayLike)
[ 1, 2, 'something', 'else' ]

搜索

  • arr.indexOf(item, from)从索引 from 开始搜索 item,如果找到则返回索引,否则返回 -1。
  • arr.lastIndexOf(item, from)—— 和上面相同,只是从右向左搜索。
  • arr.includes(item, from)—— 从索引 from 开始搜索 item,如果找到则返回 true(译注:如果没找到,则返回 false)。

find/findIndex

let result = arr.find(function(item, index, array) {
  // 如果返回 true,则返回 item 并停止迭代
  // 对于假值(falsy)的情况,则返回 undefined
});

数组可以拍平,一般是flat

var arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]

var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

var arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]

//使用 Infinity,可展开任意深度的嵌套数组
var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

具体我们可以参考:

[MDN flat documentation]: 包含使用和相关的替代方案

此外还有一个小技巧,ArrayArray.of

> Array(7)
[ <7 empty items> ]
> Array.of(7)
[ 7 ]
> Array.of(null)
[ null ]
> Array.of(undefined)
[ undefined ]

迭代

通过 [Symbol.iterator]` 实现 range

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

for (let num of range) {
  console.log(num); // 1, 然后是 2, 3, 4, 5
}
  • Iterable如上所述,是实现了 Symbol.iterator 方法的对象。
  • Array-like是有索引和 length 属性的对象,所以它们看起来很像数组。

可以使用Array.from对一个 string 类型的变量转换成 array。 比如:

> let chars = Array.from("hello world")
> chars
[
  'h', 'e', 'l', 'l',
  'o', ' ', 'w', 'o',
  'r', 'l', 'd'
]

Map

  • new Map()—— 创建 map。
  • map.set(key, value)—— 根据键存储值。
  • map.get(key)—— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。
  • map.has(key)—— 如果 key 存在则返回 true,否则返回 false。
  • map.delete(key)—— 删除指定键的值。
  • map.clear()—— 清空 map。
  • map.size—— 返回当前元素个数。

map[key] 不是使用 Map 的正确方式。 应当使用setget

map 迭代

如果要在 map 里使用循环,可以使用以下三个方法:

  • map.keys()—— 遍历并返回所有的键(returns an iterable for keys),
  • map.values()—— 遍历并返回所有的值(returns an iterable for values),
  • map.entries()—— 遍历并返回所有的实体(returns an iterable for entries)[key, value],for…of 在默认情况下使用的就是这个。

此外还有两个比较方便的方法,用于 map 和 object 之间的转换

  • Object.fromEntries 将 map 转换成 Object
  • Object.entries 生成[[key, value], [key, value]]可以用于创建 map

Set

  • new Set(iterable)—— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。
  • set.add(value)—— 添加一个值,返回 set 本身
  • set.delete(value)—— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false。
  • set.has(value)—— 如果 value 在 set 中,返回 true,否则返回 false。
  • set.clear()—— 清空 set。
  • set.size—— 返回元素个数。

WeakMap / WeakSet

WeakMap的键只能是object。 当在 object 被清理时,对应的 weakmap 中的内容也会被清理。 weakmap 适合在:

  • 需要有缓存,当一个函数的结果需要被记住(“缓存”),这样在后续的对同一个对象的调用时,就可以重用这个被缓存的结果。当对象被垃圾回收时,对应的缓存的结果也会被自动地从内存中清除。
  • 额外的数据,将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除

注意, weakmap 只支持如下的几个方法:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

这两个数据结构均不支持迭代。所以需要在合适的时机使用它们。

Object.keys, values, entries

Object.keys/values/entries 会忽略 symbol 属性

Object.fromEntries(
  // 转换为数组,之后使用 map 方法,然后通过 fromEntries 再转回到对象
  Object.entries(prices).map(([key, value]) => [key, value * 2])
);

数组解构

let arr = ["Firstname", "Lastname"]
let [firstname, lastname] = arr

如果不想要某个元素可以使用,来忽略对应位置上的内容

let [a, ,c] = ['a', 'b', 'c']
console.log(c)

实际上等号右侧可以是任何可迭代的内容,也可以是任何可以赋值的内容。

任何剩余的内容,可以使用...来接收。比如:

let [a, b, ...c] = [1,2,3,4,5,6,7]
console.log(c) = [3,4,5,6,7]

数组解构赋值的默认值是undefined

结构解构

我们一般使用结构的字段名来直接获取对应的字段的 value。 也可以将对应的字段的值赋值给自定义的变量。

let options = {
  title: 'menu',
  width: 100,
  height: 200
}

let {title, width: w, height: h} = options 

对于不一定存在的字段,我们可以使用=设置默认值。这样避免因为字段不存在而报错。

let options = {
  title: 'menu'
}

let {width = 100, height = 200, title} = options
//同样可以做到重命名

let {width: w = 100, height: h = 200, title} = options

对应剩余的内容一样可以使用...

可以使用前套的解构,一个基本的例子:

let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ['hello', 'world'],
  extra: true
}

let { size: {width, height}, items:[item1, item2], noexist=true} = options

智能函数参数

一个函数需要接收很多的参数时候,我们一般通过传递一个结构到函数内,此时可以使用结构或者是嵌套解构的方式进行:

function showMenu({ title = "Menu", width: w = 100, height = 200 } = {}) {
  alert( `${title} ${w} ${height}` );
}

showMenu(); // Menu 100 200

时间

let date = new Date()
// 时间戳,带毫秒
date.getTime() // 或者使用 +date 效果一样
// 增加 24 小时,单位毫秒
let onedayLater = new Date(24 * 3600 * 1000)
// 解析时间
let date = new Date('2022-01-02')

//可以完全指定时间
new Date(2011, 0, 1, 0, 0, 0, 0) //1 Jan 2011, 00:00:00

一些比较方便的访问日期的组件:

  • getFullYear()
  • getMonth() // 0-11
  • getDate() // 只返回 1-31 之间的数字
  • getHours()
  • getMinutes()
  • getSeconds()
  • getMilliseconds()
  • getDay() 返回一周中的第几天 0-6, 0表示周日

同时,我们可以使用一些设置时间的函数对时间进行设置。这些主要是用来配置一些指定的时间

  • setFullYear(year, [month], [date])
  • setMonth(month, [date])
  • setDate(date)
  • setHours(hour, [min], [sec], [ms])
  • setMinutes(min, [sec], [ms])
  • setSeconds(sec, [ms])
  • setMilliseconds(ms)
  • setTime(milliseconds)(使用自 1970-01-01 00:00:00 UTC+0 以来的毫秒数来设置整个日期)

两个时间可以直接相减,结果是 ms 的时间差。 一个更快用于计算时间差的方式是:

let start = Date.now()
....
let end = Date.now()

let diff = end-start

可以使用Date.parse(str)来解析日期, 字符串的格式应该为:YYYY-MM-DDTHH:mm:ss.sssZ,其中:

  • YYYY-MM-DD —— 日期:年-月-日。
  • 字符 “T” 是一个分隔符。
  • HH:mm:ss.sss —— 时间:小时,分钟,秒,毫秒。
  • 可选字符 ‘Z’ 为 ±hh:mm 格式的时区。单个字符 Z 代表 UTC+0 时区。

JSON

一般我们使用JSON.parseJSON.stringify这两个方法。 其中JSON.stringify有两个不太常用参数。JSON.stringify(value, replacer, spaces)

  • replacer
let object = {
  title: 'Hello',
  number: 10,
  arr: [1,2,3]
}

// 使用数组
JSON.stringify(object, ['arr'])  // '{"arr":[1,2,3]}'

// 使用函数
JSON.stringify(object, function replacer(key, value) {
  console.log(`${key}: ${value}`);
  return (key == 'arr') ? undefined : value;
})   // '{"title":"Hello","number":10}'
  • space 主要是用来进行美化输出的。

自定义 toJSON

像 toString 进行字符串转换,对象也可以提供 toJSON 方法来进行 JSON 转换。如果可用,JSON.stringify 会自动调用它。

let room = {
  number: 23,
  toJSON() {
    return this.number;
  }
};

let meetup = {
  title: "Conference",
  room
};

JSON.stringify(room) // 23

JSON.stringify(meetup)
/*
  {
    "title":"Conference",
    "room": 23
  }
*/

JSON.parse

同样的,JSON.parse 是可以接收一个 receiver 参数的,用于解析一些特殊字段,或者是需要自定义处理的内容。比如:

let schedule = `{
  "meetups": [
    {"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
    {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
  ]
}`;

schedule = JSON.parse(schedule, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

注意, replacer 的第一次会返回一个 key 是空,value 是自身的结果,这个在操作的时候需要考虑到。

函数

我们可以通过functionname.name来获取一个函数的名称。比如:

function sayYeah() {
  console.log('yeah')
}

console.log(sayYeah.name) // sayYeah

可以通过length来获取到函数的参数,不过 rest 的参数不会被统计。

function f1(a,b, ...c) {}

console.log(f1.length) // 2

可以给函数自定义一些变量,这样可以做到在函数外部也是可以访问到对应的变量的,这个和闭包是有所不同的。

function makeCounter() {

  function counter() {
    return counter.count++
  }

  counter.count= 0
  return counter
}


let counter = makeCounter()
console.log(counter())  // 0
console.log(counter())  // 1

console.log(counter.count) // 2
console.log(counter.count) // 2

函数的 NFE(Named Function Expression), 可以在函数被赋值了另外的一变量的时候仍然可以调用。

let sayhi = function func(name) {
  if(name) {
    console.log(name)
  } else {
    func("default")
  }
}

let sayhi2 = sayhi
let welcome = sayhi
welcome()
sayhi = null

sayhi2()

setTimeout / setInterval

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
  • setTimeout 允许我们将函数推迟到一段时间间隔之后再执行。
  • setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。
let tid = setTimeout(() => { console.log("say hello")}, 5000);
clearTimeout(tid) // 取消推迟执行

嵌套的 setTimeout 能够精确地设置两次执行之间的延时,而 setInterval 却不能。

这边需要注意的是setInterval是一个固定间隔,如果执行的函数耗时超过这个间隔,setInterval 还是会在指定时间内调用函数。 而用两次嵌套的 setTimeout 可以比较均匀一些。

let i = 1;
setTimeout(function run() {
  func(i++);
  setTimeout(run, 100);
}, 100);

注意,例如,浏览器内的计时器可能由于许多原因而变慢:

  • CPU 过载。
  • 浏览器页签处于后台模式。
  • 笔记本电脑用的是电池供电(译注:使用电池供电会以降低性能为代价提升续航)。

所有这些因素,可能会将定时器的最小计时器分辨率(最小延迟)增加到 300ms 甚至 1000ms,具体以浏览器及其设置为准。

任何 setTimeout 都只会在当前代码执行完毕之后才会执行。

对象属性配置

可以通过Object.getOwnPropertyDescriptor(obj, propertyName)获取一个对象的指定字段的属性描述信息。 同时可以使用Object.defineProperty(obj, propertyName, descriptor)去设置属性。

这边一般会涉及到三个标志:

  • writable
  • enumerable
  • configurable

使用defineProperty可以设置这些标志。 当某个属性的enumerable被设置成 false 时,则该属性将不会出现在for ... in ...中。 当我们将configurable设置成 false 时,则我们无法删除该属性,且该过程不可逆。

此外我们可以使用defineProerties来设置多个属性的标志。同理可以使用getOwnPropertyDescriptors获取多个属性的。

可以使用如下方式进行 object 的克隆

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

对象还包含两个访问器属性,gettersetter。即如下的代码:

let obj = {
  get propName() {

  }

  set propName() {

  }
}

一个 Object 的属性不能既是访问器,有是数据属性,比如下面的方式就是错误的。

//error code
Object.defineProperty({}, 'prop', {
  get() {
    return 1
  }

  value: 2
})

同样访问属性的描述符也可以使用defineProperty来设置。除了valuewritable这两个不能设置外,我们可以设置get,set,enumerableconfigurable这四个。

原型

可以使用__proto__设置,也可以通过getPrototypeOfsetPrototypeOf来代替__proto__的设置方式。

for ... in循环也会迭代继承的属性。但是Object.keys只会返回自己的 key。

可以使用obj.hasOwnProperty(key)的方式来判断 key 是否是 obj 自身的,而不是痛殴继承获得的。

F.prototype

当我们在使用new关键字创建一个新的对象时,同样可以通过继承的方式来获取属性。

function Rabbit(name) {
  this.name = name
}

let animal = {
  eats: true
}

Rabbit.prototype = animal

let rabbit = new Rabbit("White Rabbit")
console.log(rabbit.eats) // true

每个函数都有构造器属性,默认的 prototype 只有一个属性constructor的对象,这个属性指向函数本身。

prototyepe 属性在常规的对象上没有什么特殊的含义,只是一个普通的字段,但是在函数里,就有了特殊意义了

原生的原型

  • 所有的内建对象都遵循相同的模式(pattern):
    • 方法都存储在 prototype 中(Array.prototype、Object.prototype、Date.prototype 等)。
    • 对象本身只存储数据(数组元素、对象属性、日期)。
  • 原始数据类型也将方法存储在包装器对象的 prototype 中:Number.prototype、String.prototype 和 Boolean.prototype。只有 undefined 和 null 没有包装器对象。
  • 内建原型可以被修改或被用新的方法填充。但是不建议更改它们。唯一允许的情况可能是,当我们添加一个还没有被 JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准时,才可能允许这样做。

如果要将一个用户生成的键放入一个对象,那么内建的protogetter/setter 是不安全的。因为用户可能会输入 “proto” 作为键,这会导致一个 error,虽然我们希望这个问题不会造成什么大影响,但通常会造成不可预料的后果。

因此,我们可以使用 Object.create(null) 创建一个没有proto的 “very plain” 对象,或者对此类场景坚持使用 Map 对象就可以了。