JavaScript 学习笔记 (二)
前言
本文是通过 廖雪峰的 JavaScript 教程 学习而来. 全文内容几乎与教程无差别, 即本人跟着教程文字和代码均手敲一边, 遂产生此笔记. 笔记文件太大, 遂拆分成多个部分, 这是第二部分, 主要学习了函数, 包括作用域、对象方法、高阶函数、箭头函数、闭包等.
2. 函数
2.1 定义和调用
2.1.1 定义函数
JavaScript 中, 定义函数的方式如下:
1 | function abs(x) { |
上述 abs()
函数的定义如下:
function
是函数定义关键字;abs
是函数的名称( 存在匿名函数没有名称 );(x)
括号内是传入函数的参数, 多个参数用,
间隔;{ ... }
之间的代码是函数体, 可以包含若干语句, 甚至没有语句.
注意 : 函数体内部语句执行时, 一旦执行了 return
, 函数就执行完毕, 并将结果返回. 因此, 函数内部通过条件判断和循环可实现非常复杂的逻辑.
如果没有 return
语句, 函数执行完毕后也会返回结果, 只是结果是 undefined
.
由于 JavaScript 的函数也是一个对象, 上述 abs()
函数实际上是一个函数对象, 而函数名 abs
可以视为指向该函数的变量.
因此, 第二种定义方式如下:
1 | let abs = function (x) { |
这种方式下, function (x) { ... }
是一个匿名函数, 没有函数名. 但是将它赋值给了变量 abs
, 所以通过变量 abs
可以调用该函数.
2.1.2 调用函数
调用函数时, 按顺序传入参数:
1 | abs(10); // 10 |
由于 JavaScript 允许传入任意个参数而不影响调用, 因此传入的参数比定义的参数多也没问题, 虽然函数内部并不需要这些参数:
1 | abs(10, 'balabala'); // 10 |
传入参数比定义参数的个数少也没有问题:
1 | abs(); // 返回 NaN, 因为 参数 x 接收到的是 undefined , -x 无法计算所以为 NaN |
检查参数 :
1 | function abs(x) { |
2.1.3 arguments
JavaScript 还有一个关键字 arguments
, 它在函数内部起作用, 并且永远指向当前函数的调用者传入的所有参数. arguments
类似 Array, 但它不是一个 Array:
1 | function foo(x) { |
利用 arguments
可以获得调用者传入的所有参数, 也就是说, 即便不定义任何参数, 还是可以拿到参数的 值 :
1 | function abs() { |
实际上 arguments
最常用于判断传入参数的个数, 如下:
1 | // foo(a[, b], c) |
要把中间的参数 b
变为可选参数, 就只能通过 arguments
判断, 并重新赋值.
2.1.4 rest 参数
由于 JavaScript 函数允许接收任意个参数, 于是我们就不得不用 arguments
来获取所有参数:
1 | function foo(a, b) { |
为了获得除了已定义的 a
和 b
之外的参数, 我们不得不用 arguments
, 并且循环从索引 2 开始以便排除前两个参数, 这种写法特别别扭, 只是为了获得额外的 rest
参数, 有什么更好的办法?
ES6 标准引入了 rest 参数 , 上面的函数可以写成:
1 | function foo(a, b, ...rest) { |
rest 参数只能写在最后, 前面用 ...
标识, 从运行结果可知, 传入的参数先绑定 a
和 b
, 多余的参数以数组形式交给变量 rest
, 所以不再需要 arguments
便可以获取全部参数.
如果传入的参数不够正常定义的参数, rest 参数会接收一个空数组而不是 undefined
.
2.2 作用域与解构赋值
2.2.1 作用域
JavaScript 中, 通过 var
定义的变量实际上是有作用域的. 如果一个变量在函数体内部声明, 则该变量的作用域为整个函数体, 在函数外不可引用:
1 | function foo() { |
不同函数内部的同名变量相互独立, 互不影响:
1 | function foo() { |
嵌套函数中, 内部函数可以访问外部函数定义的变量, 反之不行:
1 | function foo() { |
JavaScript 的函数在查找变量时从自身函数定义开始, 从”内”向”外”查找, 如果内部函数定义了与外部函数重名的变量, 则内部变量将 屏蔽 外部变量:
1 | function foo() { |
2.2.2 变量提升
JavaScript 的函数定义有个特点, 它会扫描整个函数体的语句, 把所有用 var
声明的变量”提升”到函数顶部:
1 |
|
虽然是 strict 模式, 但语句 var x = 'Hello, ' + y;
并不报错, 因为变量 y
在后面通过 var
声明了, 然而 console.log()
打印 y
的值是 undefined
, 这是因为 JavaScript 引擎自动 提升了变量 y
的声明 , 但并 没有提升变量的赋值 , 对于上述 foo()
函数, 实际上执行的是:
1 | function foo() { |
由于 JavaScript 的这一怪异特性, 定义变量时, 必须定义到用到变量的位置之上, 最常见的是在函数内顶部用一个 var
声明函数内部用到的所有变量:
1 | function foo() { |
2.2.3 全局作用域
不在任何函数内定义的变量就具有全局作用域, 实际上, JavaScript 默认有一个全局对象 window
, 全局作用域的变量实际上是被绑定到 window
的一个属性:
1 | var course = 'Learn JavaScript'; |
以变量方式 var foo = function () {}
定义的函数实际上也是一个全局变量, 因此, 顶层函数的定义也被视为一个全局变量, 并绑定到 window
对象:
1 | function foo() { |
实际上, alert()
函数也是一个 window
的属性:
1 | window.alert('调用 window.alert()'); |
JavaScript 只有一个全局变量 . 任何变量 ( 包括函数 ) , 如果没有在当前函数作用域中找到, 就会继续向上查找, 如果在全局作用域中也没有找到, 就会报 ReferenceError
错误.
2.2.4 命名空间
全局变量会绑定到 window
上, 不同的 JavaScript 文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数, 都会造成命名冲突, 并且很难发现.
减少冲突的一个办法是, 把自己的所有变量和函数全部绑定到一个全局变量中, 例如:
1 | // 唯一的全局变量 |
把所有的代码全部放入唯一的命名空间 MyApp
中, 会大大减少全局变量冲突. jQuery、YUI、underscore 等 JavaScript 库都是这么做的.
2.2.5 局部作用域
由于 JavaScript 的变量作用域实际上是 函数内部 ( 不是循环内部 ) , 在 for
循环语句中无法定义具有局部作用域的变量:
1 | function foo() { |
ES6 通过引入 let
关键字解决这一问题, 用 let
替代 var
可以声明一个块级作用域变量:
1 | function foo() { |
2.2.6 常量
由于 var
和 let
声明的是变量, 如果声明一个常量, ES6 之前是不行的, 以前的做法是使用全部大写的变量名自认为其是常量, 不能修改它的值:
1 | let PI = 3.1415; |
ES6 引入关键字 const
来定义常量, const
与 let
都具有块级作用域:
1 | const PI = 3.1415; |
2.2.7 解构赋值
从 ES6 开始, JavaScript 引入了解构赋值, 同时对一组变量进行赋值.
传统方式, 如何把一个数组的元素分别赋值给几个变量:
1 | let array = ['hello', 'JavaScript', 'ES5']; |
ES6 可以通过解构赋值, 简化过程:
1 | let [x, y, z] = ['hello', 'JavaScript', 'ES6']; |
解构还可以 忽略某些元素 :
1 | let [, , z] = ['hello', 'JavaScript', 'ES6']; |
从对象中取出若干属性, 也可以使用解构赋值, 便于快速获取对象的指定属性:
1 | let person = { |
如果要使使用的 变量名和属性名不一致 , 可以使用 :
方法:
1 | let person = { |
注意 : age
不是变量, 而是对象的属性, nianling
才是变量.
解构赋值还可以使用 默认值 , 这样可以避免属性返回 undefined
的问题:
1 | let person = { |
当变量已经被声明了, 再次用结构赋值, 将会报错:
1 | let x, y; |
2.2.8 使用场景
交换赋值
1
2let x = 1, y = 2;
[x, y] = [y, x]; // 至少在冒泡排序等算法中可以少写两行代码快速获取当前页面的域名和路径
1
let { hostname:domain, pathname:path } = location;
函数使用对象作为参数, 解构直接把属性绑定到变量中
1
2
3
4
5
6
7function buildDate({year, month, day, hour=0, minute=0, second=0}) {
return new Date(`${year}-${month}-${year} ${hour}:${minute}:${second}`);
}
// 方便之处是可以只传入 year, month, day 三个参数, 也可以全部传入
buildDate({ year: 2017, month: 1, day: 1 });
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
2.3 方法
2.3.1 定义方法
JavaScript 中, 在一个对象中绑定函数, 称为这个对象的方法:
1 | let xiaoming = { |
绑定到对象上的函数称为方法, 和普通函数没有什么区别, 但是它在内部使用了一个 this
关键字. 在一个方法内部, this
是一个特殊变量, 它始终指向当前对象, 也就是 xiaoming
这个变量. 所以 this.birth
可以拿到 xiaoming
的 birth
属性.
2.3.2 设计缺陷
拆开写:
1 | function getAge() { |
注意 : 这是 JavaScript 的一个大坑, 单独调用 getAge()
返回 NaN
, 以对象方法的形式调用 xiaoming.age()
返回 23
. 这是 this
的问题.
当在对象方法中使用 this
时, this
指向被调用的对象, 也就是 xiaoming
, 而在函数中使用 this
, 比如 getAge()
, 此时, this
指向全局对象也就是 window
.
如果把 xiaoming.age
赋值给变量, 通过 变量名
调用, 也是不符合预期的:
1 | let fn = xiaoming.age; |
如果要使 this
指向正确, 必须使用 obj.func()
的形式调用方法.
注意 : 这是一个巨大的设计缺陷, ECMA 决定在严格模式下, 函数的 this
指向 undefined
, 因此, 在严格模式下, 会得到一个错误:
1 | ; |
如果你在方法中使用函数, 函数中使用 this
,仍然会报错, 因为这样使用 this
仍然指向 undefined
, 在非 strict
模式下指向 window
.
1 | ; |
2.3.3 拯救 this
通过一个额外的变量 that
保存 this
的正确指向才可以修复这个问题:
1 | ; |
2.3.4 apply
我们可以根据是否是 strict
模式, 判断 this
指向的是 undefined
还是 window
, 也可以直接控制 this
的指向.
使用函数本身的 apply
方法, 可以指定函数的 this
指向那个对象. 它接收两个参数, 第一个参数就是要绑定的 this
变量, 第二个参数是 Array
表示函数本身的参数:
1 | function getAge() { |
2.3.5 call
call
与 apply
唯一的区别是, apply()
把参数打包成 Array
再传入, 而 call()
把参数按顺序传入.
1 | Math.max.apply(null,[3, 5, 3]); // 5 |
对于普通函数, 我们把 this
绑定到 null
.
2.3.6 装饰器
利用 apply()
, 我们还可以动态改变函数的行为. JavaScript 的所有对象都是动态的, 即使内置的函数, 也可以重新指定新的函数.
假设要统计一下代码调用了 parseInt()
多少次, 可以把所有的调用都找出来, 然后 count += 1
, 这样太 low 了, 最佳方法是用我们自己的函数替换掉默认的 parseInt()
:
1 | ; |
2.4 高阶函数
高阶函数 ( Higher-order Function ). JavaScript 的函数可以指向某个变量, 既然变量可以指向函数, 函数的参数能接受变量, 那么函数就可以把函数作为参数, 这种函数套函数的形式就是高阶函数. 例如:
1 | function add(x, y, f) { |
2.4.1 map
要对数组的每一个元素 分别 进行 相同的计算 , 就可以用 map
而不是循环, 例如, 现有函数 f(x) = x^2
, 数组 [1, 2, 3, 4]
, 要把数组每一个元素都进行 f(x)
计算, 可以用 map
实现:
1 | function f(x) { |
map()
作为一个高阶函数, 它把运算规则抽象化了. 不仅仅可以进行简便的计算, 还可以进行复杂的计算, 例如, 把 Array 的所有数字转变为字符串:
1 | let arr = [1, 2, 3, 4]; |
2.4.2 reduce
要对数组的所有元素进行计算, 比如求和, 求阶乘等, 可以看成全部元素做一个计算, 也可以看成每一个元素和下一个元素进行计算再将结果与再下一个元素进行计算直至结束. 这种情况可以使用 reduce
. 效果如下:
1 | [x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4) |
例如求阶乘:
1 | const arr = Array.from({ length: 4 }, (_, i) => i + 1); |
reduce
可以实现 join
的效果, 例如将 [1, 2, 3, 4]
转变为 1-2-3-4
, 方法如下:
1 | const arr = Array.from({ length: 4 }, (_, i) => i + 1); |
2.4.3 filter
顾名思义, filter
用于过滤掉 Array 的某些元素, 然后返回剩下的元素. 和 map
相似, filter
分别对每一个元素进行相同的计算 ( 判断 ), 然后根据返回值是 true
还是 false
决定保留还是丢弃元素.
例如, 在一个数字 Array 中, 只保留偶数:
1 | let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; |
2.4.4 回调函数
filter()
接收的回调函数, 可以有多个参数. 通常只取第一个参数( 元素本身 ), 回调函数还有另外两个参数, 表示元素的索引和数组本身:
1 | let arr = ['A', 'B', 'C']; |
利用 filter()
可以去除 Array 的重复元素:
1 | let res, |
2.4.4 sort
JavaScript 的 Array 中的 sort()
方法是用于排序的, 但是排序的结果可能令人奇怪:
1 | ['Google', 'Apple', 'Microsoft'].sort(); |
第二个排序 apple
排到了最后面是因为大写字母的 ASCII 码全部在小写字母的前面, 所以 G
和 M
都在 a
前面.
第三个排序什么情况? 原来是 sort()
默认把所有元素先转换成 String 类型再排序, 结果 10
排在了 2
前面, 因为 1
的 ASCII 码比 2
的小.
实际上 sort()
也是一个高阶函数, 它可以接收一个比较函数实现自定义排序, 按从小到大顺序写:
1 | let arr = [10, 2, 1, 20]; |
默认对字符串排序是区分大小写的, 如果需要忽略大小写, 只需要把字符串全部转化成大写或者小写即可:
1 | let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft']; |
注意 : sort()
方法会直接修改 Array 并返回当前 Array;
1 | let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft']; |
2.4.5 every
every()
方法可以判断数组所有元素是否满足条件, 例如判断字符串数组是不是每个字符都是小写:
1 | let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft']; |
2.4.6 find
find()
方法用于查找符合条件的第一个元素, 如果找到了返回这个元素, 否者返回 undefined
:
1 | let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft']; |
2.4.7 findIndex
findIndex()
和 find()
类似, 也是找到第一个符合规则的元素, findIndex()
返回元素的索引, 找不到返回 -1
:
1 | let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft']; |
2.4.8 forEach
forEach()
和 map()
类似, 它把传入的函数依次作用于每个元素, 但不会返回新数组. forEach()
通常用于遍历数组, 所以传入的函数不需要返回值:
1 | let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft']; |
2.5 闭包
函数除了可以接收一个函数作为参数外, 还可以将函数作为结果返回出去. 例如, 实现一个对 Array 的求和, 一般求和函数如下:
1 | function sum(arr) { |
如果不需要立刻求和, 而是在后面的代码中根据需要再计算怎么办?
可以不返回求和的结果, 而是返回求和的函数:
1 | function lazy_sum(arr) { |
当调用 lazy_sum()
时返回的是求和的函数而不是求和结果:
1 | let f = lazy_sum([1, 2, 3, 4, 5]); |
这个例子中, 我们在函数 lazy_sum
中又定义了一个函数 sum
, 并且, 内部函数 sum
可以引用外部函数 lazy_sum
的参数和局部变量, 当 lazy_sum
返回函数 sum
时, 相关参数和变量都保存在返回的函数中, 这种称为 “闭包 ( Closure )”的程序结构具有很大的作用.
当调用 lazy_sum
时每一次返回的是一个新的函数, 即使传入相同的参数, 返回的函数也不相同:
1 | let f1 = lazy_sum([1, 2, 3]); |
f1()
和 f2()
的调用结果也互不影响.
另一个需要注意的问题是, 返回的函数并没有立刻执行,而是直到调用了 f()
才执行.
例子:
1 | function count() { |
每次循环生成一个函数存储到 arr
中, 最终返回元素是函数的数组, 被 results
接收, 看上去感觉 f1()
、f2()
、f3()
的结果应该是 1
、4
、9
, 然而实际结果是:
1 | f1(); // 16 |
这是因为返回函数引用了用 var
定义的变量 i
, 但它并非立刻执行, 等到三个函数返回时, 它们所引用的变量 i
已经变成了 4
, 再调用计算时便全部使用 4
, 得到 16
.
注意 : 返回闭包时, 一定 不要引用任何循环变量 , 也不要引用后续会发生变化的变量.
如果一定要引用循环变量, 可以 再创建一个函数 , 用该函数的参数绑定循环变量的当前值, 无论循环变量后续如何更改, 已绑定的函数参数不会变化:
1 | function count() { |
这里的 function (x) { return x * x } (3);
是一个立即执行的 匿名函数 , 需要用括号包裹, 否者报错:
1 | (function (x) { return x * x } (3)); |
另一个方法是把循环变量 i
用 let
声明在 for
循环体中, let
作用域决定了在每次循环时都会绑定新的 i
:
1 | function count() { |
如果将 i
定义在 for
循环的外面, 则仍然是错误的:
1 | function count() { |
因此 , 最好的办法就是在闭包中不使用后续会发生变化的变量, 否者很难调试.
强大功能 : 在面向对象的设计语言中, 要在对象内部封装一个私有变量, 可以用 private
修饰一个成员变量, 在没有 class
机制, 只有函数的语言里, 借助闭包, 可以封装一个私有变量, 例如, 设计一个计数器:
1 | function create_counter(initial) { |
在返回的对象中, 实现了一个闭包, 携带了局部变量 x
, 且从外部代码是无法访问到 x
的, 也就是说闭包是携带状态的函数, 并且它的状态可以完全隐藏起来.
闭包还可以把多参数的函数转变成单参数的函数. 例如要计算 x^y
可以用 Math.pow(x, y)
函数, 不过考虑到经常计算 x^2
和 x^3
, 我们可以利用闭包创建新的函数 pow2
和 pow3
:
1 | function make_pow(n) { |
2.6 箭头函数
2.6.1 定义方式
ES6 标准引入了一种新的函数: 箭头函数 ( Arrow Function ).
箭头函数相当于匿名函数, 并且简化了函数定义. 有以下三种形式
1 | // 三种形式 |
this
箭头函数看上去是一种匿名函数的简写, 但是实际上, 箭头函数和匿名函数有明显的区别, 箭头函数内部的 this
是词法作用域, 由上下文决定, 之前的例子中, 由于 JavaScript 对 this
绑定的错误处理, 下面的例子无法获得预期结果:
1 | let obj = { |
现在, 箭头函数完全修复了 this
的指向问题, this
总是指向词法作用域, 也就是外层调用者 obj
:
1 | let obj = { |
实操 : 使用箭头函数简化排序传入的函数:
1 | let arr = [10, 20, 1, 2]; |
2.7 标签函数
2.8 生成器
3. 标准对象
3.1 Date
3.1.1 获取时间
在 JavaScript 中, Date
对象是表示时间和日期的. 获取系统时间的方法如下:
1 | let now = new Date(); |
3.1.2 创建 Date
如果要创建一个指定日期和时间的 Date
对象:
1 | let d = new Date(2015, 5, 19, 20, 15, 30, 123); |
注意 : JavaScript 作者脑抽, Date
对象 Month 是从 0 开始的, 即 1-12月对应 Month 0-11.
另一种创建方法是解析一个符合 ISO 8601
格式的字符串:
1 | let d = Date.parse('2015-06-24T19:49:22.875+08:00'); |
时区 , Date
对象总是按浏览器所在时区显示的, 可以调整时区:
1 | llet d = new Date(1435146562875); |
3.2 RegExp
3.2.1 正则表达式
正则表达式是用来匹配字符串的强大工具, 他的思想是用一种描述性的语言给字符串定义一个规则, 凡是符合规则的字符串, 认为匹配成功, 否则就是不合法的.
正则表达式本身也是一个字符串, 在正则表达式中如果直接给出字符, 就是精确匹配, 用 \d
可以匹配一个数字, \w
可以匹配一个字母或者数字, .
可以匹配任意字符, \s
匹配一个空格,
要匹配长度不定的字符串, 在正则表达式中, 用 *
表示任意个字符 ( 包括 0 个 ), 用 +
表示至少一个字符, 用 ?
表示 0
或 1
个字符, 用 {n}
表示 n
个字符, 用 {n, m}
表示 n
- m
个字符.
例如要匹配手机号, 手机号的规则是 11 位, 第二位从 3 到 9:
1 | '1[3-9]\d{9}' |
如果要匹配带区号的电话号, 三位区号加 3 到 8 位号码:
1 | '\d{3}\-\d{3, 8}' |
3.2.2 进阶
要更精确的匹配, 需要用到 []
表示范围, |
表示或者, ^
表示行的开头, $
表示行的结束, 例如:
[0-9a-zA-Z\_]
表示匹配一个数字、字母或者下划线 ( 表达式[]
内的\
用作转义 )[0-9a-zA-Z\_]+
表示至少匹配一个数字、字母或者下划线, 比如a100
、0_Z
等[a-zA-z\_\$][0-9a-zA-Z\_\$]*
表示匹配由字母或下划线开头, 后面接任意个由一个数字、字母或者下划线组成的字符串, 也就是 JavaScript 允许的变量名
3.2.3 RegExp
JavaScript 中有两种创建正则表达式的方法, 第一种是直接通过 /正则表达式/
写出来, 第二种是通过 new RegExp('正则表达式')
创建一个 RegExp
对象.
1 | let re1 = /ABC\-001/; |
尝试 :
1 | let re = /^\d{3}\-\d{3,8}$/; |
切分字符串
常规 str.split(' ')
切分字符串无法识别连续的空格, 通过正则表达式可以灵活的切分字符串:
1 | 'a b c'.split(' '); // ['a', 'b', '', 'c'] 第三个元素是空 '' |
分组
除了单纯的匹配以外, 正则表达式还有提取子串的强大功能, 用 ()
表示就是要提取的分组 ( Group ), 例如:
1 | let re = /^(\d{3})-(\d{3,8})$/; |
如果正则表达式中定义了组, 就可以在 RegExp
对象上用 exec()
方法提取出子串来. exec()
方法在匹配成功后, 会返回一个 Array, 第一个元素是正则表达式匹配到的整个字符串, 后面的字符串表示匹配成功的子串. 失败时返回 null
.
贪婪匹配
正则表达式默认是贪婪匹配, 就是尽可能匹配更多的字符. 例如, 匹配出数字后面的 0
:
1 | let re = /^(\d+)(0*)$/; |
由于 \d+
采用贪婪匹配, 直接把 0
也匹配上了, 所以 0*
匹配到了空字符串. 必须让 \d+
采用非贪婪匹配 ( 也即是尽可能匹配更少的字符 ), 才能把后面的 0
匹配出来, 加个 ?
就可以让 \d+
采用非贪婪匹配:
1 | let re = /^(\d+?)(0*)$/; |
全局搜索
JavaScript的正则表达式还有一些特殊标志, 最常用的是 g
, 表示全局匹配:
1 | let r1 = /test/g; |
全局匹配多次进行 exec()
方法来搜索一个匹配的字符串, 当我们指定 g
标志后, 每次运行 exec()
, 正则表达式本身会更新 lastIndex
属性, 表示上一次匹配到的最后的索引:
1 | let s = 'JavaScript, VBScript, JScript and ECMAScript'; |
尝试 : 是不是可以写成循环呢?
1 | let s = 'JavaScript, VBScript, JScript and ECMAScript'; |
其他标志
i
标志表示忽略大小写, m
标志表示执行多行匹配.
3.3 JSON
3.3.1 介绍
JSON 是 JavaScript Object Notation 的缩写, 它是一种数据交换格式.
在 JSON 出来之前, 一直是用 XML 来传递数据, 因为 XML 是一种纯文本格式, 所以适合在网络上交换数据. XML 本身不复杂, 但是加上 DTD、XSD、Xpath、XSLT 等一大堆复杂的规范后, 变得让人头大. 花在弄懂规范上的时间剧增.
2002年, 聪明的道格拉斯·克洛克福特发明了 JSON 这一数据交换格式. JSON 实际上是 JavaScript 的一个子集, 在 JSON 中, 一共就只有以下几种数据类型:
number
: 和 JavaScript 的number
完全一致boolean
: 就是 JavaScript 的true
或false
string
: 就是 JavaScript 的string
null
: 就是 JavaScript 的null
array
: 就是 JavaScript 的Array
表示方式:[]
object
就是 JavaScript 的{ ... }
JSON 定死了字符集必须是 UTF-8, 表示多语言没有问题. 为了统一解析, JSON 的字符串必须使用 ""
, Object
的键也必须使用 ""
JSON 使用简单, 很快成为 ECMA 的标准. JavaScript 中可以直接使用 JSON, 因为内置了 JSON 的解析.
把任何 JavaScript 对象变成 JSON, 就是把这个对象序列化成一个 JSON 格式的字符串, 这样才能进行通信.
如果收到一个 JSON 字符串, 只需要对其反序列化成一个 JavaScript 对象, 就可以直接使用这个对象了.
3.3.2 序列化
1 | let xiaoming = { |
要美化输出, 可以加上参数, 按缩进输出:
1 | let xiaoming = { |
还可以传入一个函数, 对象的每个键值对都会被函数处理:
1 | let xiaoming = { |
如果需要精确控制序列化哪些项, 给 xiaoming
定义一个 toJSON()
方法, 直接返回 JSON 应该序列化的数据:
1 | let xiaoming = { |
3.3.3 反序列化
接收到一个 JSON 格式字符串, 用 JSON.parse()
可以把它变成一个 JavaScript 对象:
1 | let x = JSON.parse('{"name": "John", "age": 30, "city": "Suzhou"}'); |
JSON.parse()
可以接收一个函数, 用来转换解析出的属性:
1 | let obj = JSON.parse( |
第二部分结束