MacBook Air 安装 Windows 7 过程记录

前两天给朋友帮忙,给他的 MacBook 安装了一个 Mac + Windows 双系统。机型是 MacBook Air 13 英寸(2013 年中),由于配置并不高( CPU 1.3 GHz,内存 4G),没有选择 Windows 10,装了一个 Windows 7,运行起来非常流畅。在此记录一下全过程,以供参考。

工具:macOS 自带的 Boot Camp助理

准备:一个 16G 以上 U 盘(并且会对 U 盘格式化),原版 Windows 7 镜像文件(iso)。我使用的是 Windows 7 专业版 SP1(64位),注意需要带有 SP1 的镜像,否则可能无法安装。

关于 macOS 重装

安装之前先做了一下 macOS 的重装,因为电脑刚送来的时候无法开机。朋友不小心在 iCloud 上点了抹掉 MacBook,结果 MacBook 就进不去系统了,开机只能看到一个大大的“禁止通行”的标志,按什么都没反应。重装方法:

  • 按住 Shift-Option-Command-R 开机
  • 按住 Option-Command-R 开机

两种都可以,区别:第一种是重装出厂时自带的 macOS 版本,第二种是用重装最新版本 macOS 。我选了第一种,重装完以后是出厂的 macOS  10.9 Mavericks。

开机按下相应键,进入界面后,先输入Wi-Fi密码,等待地球仪转动,就会看到

IMG_6864

直接选 “Reinstall OS X“,会出现选择磁盘的选项:

IMG_6866

第一次尝试的时候,磁盘那个位置是空的,没法进行下一步。这时退回上一个界面,用最下面的 Disk Utility,给分区做一下格式化(抹掉),再重新回去点下 “Reinstall OS X“,就可以看到磁盘了。剩下的就是等待自动安装,Mac 会联网下载操作系统:

IMG_6868

安装双系统步骤

步骤参照苹果官网的文档,大致如下:

  1. 先根据网页上的兼容性表格,找到你的机型,下载对应 Boot Camp 支持软件(5 或 4)
  2. 提前把 ISO 镜像文件拷贝到 Mac 里
  3. 插入 U 盘,打开 Mac 自带的 Boot Camp 助理,选中第一项,开始制作引导盘
  4. 制作完成后,把第 1 步下载的压缩包解压,将里面的内容全部拖到 U 盘,选择“覆盖”
  5. 重新打开 Boot Camp 助理,选第 3 项,接下来会提示你对硬盘进行分区
  6. 设置好 Windows 分区的大小后,Mac 就会进行分区并自动重启,进入 Windows 安装
  7. 安装开始,选择标有 BOOTCAMP 的分区
  8. 然后就是等待 Windows 7 自动安装了
  9. 安装完成进入 Windows 系统后,先找到 U 盘上的 Boot Camp 文件夹,拖到 Windows 里,双击里面的 Setup 安装 Boot Camp 驱动
  10. 驱动安装完就可以正常使用了。推荐先安装系统更新。

第一次尝试,遇到的问题:

不能安装到 BOOTCAMP 驱动器

第 7 步选择 BOOTCAMP 分区后,会有一行小字提示不能安装,点击“驱动器选项”,进行格式化,然后就能安装了。

无法验证签名

第 9 步 Windows 7 安装完成后无法正常进入系统,提示:

IMG_6904

解决方法:强制关机,再次开机并按住 Option,会出现一个菜单,选择 macOS 所在的分区(一般是最左边那个),进入 macOS 系统。

进入 macOS 后,需要安装一个 NTFS 软件(可以用 ParagonNTFS 的免费试用,或装一个 Tuxera 等等),安装好后,进入 Windows 所在的分区(因为 Windows 是 NTFS 格式,必须安装前面的软件才能进行操作),删掉 Windows/system32/drivers/AppleSSD.sys 即可。重新启动就能正常进入 Windows 了。

无法安装 Boot Camp 驱动

第 9 步安装 Boot Camp 驱动的时候,双击 Setup 出现了提示“这个版本的 Boot Camp 不适用于此电脑型号”:

boot-camp-fix-1

尝试了两个解决方法,都失败了:

1)打开 cmd,手动 cdBootCamp\Drivers\Apple 目录,然后执行 msiexec /i BootCamp.msi 来安装

2)直接双击 Boot Camp\Drivers\Apple\BootCamp.msi 安装

失败的提示:“安装包有问题,所需的DLL无法运行”

339108040

具体原因不清楚,估计是 macOS 的版本(10.9 Mavericks)太老了,于是升级了一下 macOS,在官网下载好10.10(Yosemite)的安装包,用 10.10 重新走了一遍上述过程,然后 Boot Camp 可以安装了:

IMG_6905

其他问题:

Windows下如何用右键

在安装 Boot Camp 驱动程序之前,用触控板在 Windows 里面是没法右键单击的,可以临时用 Shift+F10 组合键。

安装好 Boot Camp 以后,在右下角任务栏打开 Boot Camp 控制面板,找到“触控板”选项卡,就可以配置触控板右键了。

反转触控板滚动方向

在 Windows 用触控板上下滚动,会发现方向是反着的,修改方法:

  1. 在 Windows 控制面板 – 硬件和声音里,找到“鼠标”
  2. 点“硬件”选项卡下面的属性
  3. 点“详细信息”选项卡,在属性里选“硬件实例路径”
  4. 记住里面的值
  5. 运行 regedit
  6. 找到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\HID\VID_???\VID_???\Device Parameters,其中的问号是第 4 步里的路径值
  7. 双击 FlipFlopWheel,改为1
  8. 重启即可

如果第 6 步找不到,也可以直接搜索 FlipFlopWheel,注意改之前检查下路径是否和 6 描述的一致。

隐藏 Mac 分区

默认情况下,在 Windows 系统内,也能看到 macOS 所在的分区,会被加载成一个盘,这样容易导致对 macOS 分区文件的误操作。可以在 Windows 里将其隐藏,方法是找到 C:\Windows\System32\drivers\AppleMNT.sys 这个文件,删除或者重命名。这是分区的驱动文件,删掉以后 Windows 启动时就不会加载 macOS 所在的分区了。如果有 Windows 和 macOS 之间共享文件的需要,那这一步就可以省略。

默认引导 macOS

装完 Windows 7 后,默认开机启动的是 Windows。如果想默认启动 macOS,点击右下角的 Boot Camp 软件,选最后一项 “在 OS X 重新启动 ” 即可:

IMG_6922

这样下次就会默认启动 macOS。

另一种方法:开机时按住 Option,出现分区菜单后,先选择 macOS 所在的分区,下面会出现一个箭头,这时将鼠标指针移到箭头上,按住 Control 键,会发现箭头图标变成环状,这时点击一下,会开始启动 macOS,并且下次也默认 macOS。

关闭 Windows 7 上的自动播放

Windows 7 的自动播放功能,使用不当容易感染 U 盘病毒,可以关闭“自动播放”:

  1. 运行 gpedit.msc,打开组策略
  2. 打开本地计算机策略——用户配置——管理模板——Windows组件——自动播放策略
  3. 在右侧的列表中双击“关闭自动播放”,选择“已启用”,然后确定并重启即可。

更新 Windows 7

更新前需要先激活,具体怎么激活就不多说了,可以在地址1 地址2 找找,然后通过 https://getcid.info 电话激活。

刚安装完的 Windows 7 SP1 是没法检查系统更新的,需要先手动下载安装一部分,然后才能正常搜索更新。

先安装这个“服务堆栈更新”,里面有适合 x64 系统的下载地址(先安装这个,才能安装下面的那个)。

然后安装“方便汇总更新”,里面集合了从 SP1 至 2016 年 4 月的大部分更新,x64 版本的下载地址我提取出来了:链接。也可以自己到微软上面下载,注意选择 x64 的。(这个网页可能用到了 ActiveX,尽量用 IE 打开。)如果 IE 打开失败,先打开 Internet 选项,进“高级”选项卡,看看“安全”下面的 “TLS 1.2” 有没有勾上。勾上以后再访问。接着可能还会出现红色的“证书错误”提示,这是因为系统里面缺少根证书的更新,可以暂时不管,等装好更新就没有这个提示了。选择坚持进入网站,就能进去了,下载好这个更新包以后,等待安装完成,系统自带的检查更新功能也能正常使用了。

另外英语好的也可以参照一下这个 Youtube教程,里面给的补丁地址比较全。不过按照我上面的步骤也足够了,除非想要 ESU。视频里讲了如何获得 ESU 更新。

什么是 ESU

微软从 2020 年 1 月 14 日已经不再对 Windows 7 提供支持了,也就是后续不会再收到系统漏洞更新。启动 Windows 7 时可能会看到如下提示(可以直接关闭):

IMG_6911

但如果是企业客户,给微软掏钱就可以继续获得更新,一直到 2023 年 1 月 10 日,这就是 ESU。

Zepto 与 jQuery 的差异总结

Zepto 是一个与 jQuery 高度相似的库,API 与 jQuery 一致,优点是体积小,核心模块压缩以后不到 10K。我们知道 jQuery 的缺点就是体积大,即便到了 2.x 版本,仍然有 80 多 K。而 Zepto 非常适合在移动端代替 jQuery,并且很多功能做成了模块,可以根据需要选用。

logo

虽然 API 与 jQuery 高度一致,但实际使用中,还是有着不少差异的,如下:

.css() 方法

在 jQuery 中的 .css() 方法非常强大,可以自动替你处理各个浏览器的 CSS 前缀,比如:

$('#el').css('transform', 'scale(0.5)');

如果浏览器不支持上面的属性,jQuery 可以自动帮你加前缀,比如 -webkit-transform 等。而这一点 Zepto 还做不到。你必须自己探测浏览器支持哪种前缀,然后使用 $('#el').css('-webkit-transform', 'scale(0.5)')

另外一点,.css() 方法支持驼峰式的属性名,比如 $('#el').css('backgroundColor', 'green'),jQuery 和 Zepto 可以转换为 background-color。但是对于带前缀的属性,Zepto 就不行了,如:

$('#el').css('webkitTransform', 'scale(0.5)');

在 jQuery 可以,但是 Zepto 会把属性名转换为 webkit-transform,少了前面的 - 号。

其他几处差异

  • 使用属性选择器,$('[attr="value"]')value 必须要加引号
  • 没有 innerWidth(), innerHeight(), outerWidth(), outerHeight() 四个方法,只有 width()height(),分别返回 offsetWidthoffsetHeight
  • offset() 方法返回元素在文档中的位置,jQuery 中只有 top, left 两个属性,Zepto 多了 widthheight
  • .clone() 不支持克隆元素上的事件
  • .animate() 的参数有区别
  • 使用自定义事件时,都支持命名空间,比如 $(el).on('click.myPlugin', fn),而 Zepto 不支持事件名称出现 . 号,可以换成冒号

额外功能

有一些 jQuery 常用的功能,Zepto 的核心模块里都没有,因为出于缩小体积的考虑。必须添加额外的模块才能使用这些功能:

  • .data() 方法默认只能存储字符串,如果想要存储对象,需要 data.js 模块
  • tapswipe 事件,需要 touch.js。Zepto 的 tap 事件不是很完美,经常导致穿透现象,建议改用 FastClick
  • pinch 事件需要 gesture.js
  • $.Deferred需要 deferred.js
  • .animate() 需要 fx.js。show/hide/toggle/fadeIn/fadeOut 需要 fx_methods.js
  • .end()andSelf() 方法,需要 stack.js

如何给前端工具设置代理

在天朝,当一个开发者非常苦逼,因为说到开源,就是相互交流,而有堵墙总是在阻断这个交流的过程,不断想要把我们困在笼子里。比如你查个技术文章,这时候“CONNECTION RESET”了,装个 npm 包,一直打转,死活装不上,这时候程序员的内心都是抓狂的。因此科学上网是大家必须掌握的技能,这篇文章不是探讨这个,假设你已经有了梯子,现在介绍下各个工具比如 Sublime Text、npm 等如何设置代理。

ss

假设本地开启的 http 代理为 http://127.0.0.1:1080。(Shadowsocks 默认就是这个地址)。ss 是一个很棒的工具,虽然作者被喝茶现在不维护了,但仍然可以由别人进行下去,在此向 clowwindy 的付出表示敬意。如果用的是 Mac 版 ShadowsocksX,默认只开启了一个 SOCKS5 代理,没有 http 代理,可以利用 polipo 来转换成 http 代理,简单介绍一下方法:

先安装 Homebrew,然后执行下面命令安装 polipo:

brew install polipo

然后,修改 /usr/local/opt/polipo/homebrew.mxcl.polipo.plist 文件,找到 <string>/usr/local/opt/polipo/bin/polipo</string> 这句,在下面添加一行:

<string>socksParentProxy=localhost:1080</string>

其实是把 http 流量导到 localhost:1080 的 SOCKS 代理上。第三步,设置开机启动:

ln -sfv /usr/local/opt/polipo/*.plist ~/Library/LaunchAgents

设置完成后,还需要手动启动一下 polipo:

launchctl load ~/Library/LaunchAgents/homebrew.mxcl.polipo.plist

这时就 OK 了,polipo 生成的 http 代理,默认地址为 localhost:8123。如果想要暂时终止 polipo,可以执行:

launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.polipo.plist

Sublime Text

这里其实是给 Package Control 设置代理,方法:

打开“Preferences -> Package Settings -> Package Control”-> Settings – User,写入下面键值:

{
  "http_proxy": "127.0.0.1:1080"
}

npm

npm 其实有两个办法,一是用 cnpm,这是淘宝提供的一个 npm 镜像,好像是每 15 分钟跟 npm 同步一次。安装好了以后,你需要用 cnpm 命令来代替 npm 命令使用。安装方法(先装好 npm):

npm install -g cnpm --registry=https://registry.npm.taobao.org

用了这个就不用担心很慢的问题了。

第二个办法是给 npm 设置代理,运行以下命令行:

npm config set proxy http://127.0.0.1:1080 --global
npm config set https-proxy http://127.0.0.1:1080 --global

注意 --global 标志,表示配置被写入系统配置文件。Windows 下位于 C:\Users\用户名\AppData\Roaming\npm\etc\npmrc,Mac 下位于 ~/.npmrc)

Atom

C:\Users\用户名\.atom 下建立 .apmrc 文件(Mac 对应 ~/.atom/.apmrc),写入:

proxy = http://127.0.0.1:1080
https-proxy = http://127.0.0.1:1080

当然也可以用类似上面的 npm 命令,只要把 npm 换成 apm 就行了。

阅读全文 »

ES6 快速上手:模块

es6-small

ES6 引入了原生的模块支持。

一个 ES6 模块就是一个 JS 文件,除了以下两个区别:

  • 会自动开启严格模式
  • 可以使用 importexport 关键字

ES6 模块还有一些特点:

  • 模块中声明的变量都是私有的,即使在最外层作用域声明
  • 在模块最外层,this 指向 undefined

模块中声明的变量,默认都是私有的。如果要暴露给其他模块使用,需要 export

// mod1.js
export function foo(a, b, c) {
  ...
}
export class Bar() {
  ...
}

export 可以用在 function, class, var, let, const 前。如果输出的是上例中的 functionclass 声明,则必须有函数名或类名,不能匿名。

如果想要一次性导出多个接口,也可以这样写:

export {foo, bar};

export 语句必须放在最外层,也就是,不能在语句块中 export

import 用来导入模块:

import {foo} from "mod1.js";

或者,导入多个接口:

import {foo, bar} from "mod1.js";

mod1.js会被提前导入并加载。注意导入以后生成的变量 foobar 都是常量,类似 const。所以你不能再声明一个同名变量,或者修改 foobar

你也可以将模块的所有接口整个导入:

import * as myModule from "mod1.js";

这样 mod1.js 的所有接口都被绑定在了 myModule 对象上。你可以在 myModule 上调用 myModule.foo()

不管被导入多少次,一个模块内的代码始终只执行一次:

import { foo } from "mod1.js";
import { bar } from "mod1.js";

重命名接口

导入时,可以给接口重命名:

import {foo as bar} from "mod1.js";

导出时也可以:

export {
  foo as foo1,
  bar as bar2
};

重命名以后,只能使用新的名字。

default

每个 ES6 模块可以有一个默认导出,使用的是 default 关键字:

export default function(a, b) {
  ...
}

export default 关键字后可以跟随任何值。使用 default 时,函数可以是匿名的。

你也可以指定某一个变量为 default

let myObject = {
  foo() {
    return "foo...";
  },
  bar: 66
};
export {myObject as default};

要将一个模块的默认导出 import 进来:

import myModule from "mod1.js";

注意 myModule 没有了花括号。此时在 myModule 上就可以调用 foobar 了。

这种设计是为了与已存在的 CommonJS 和 AMD 模块相兼容。以 jQuery 为例:

import $ from "jquery";

一个模块只能有一个默认导出。但也可以同时存在非默认的导出:

// mod2.js
export let a = 123;
export default function(b, c) {
  ...
}

可以这样把两者一同导入进来:

import foo, { bar } from "mod2.js";

注意非默认的导出总是有一对花括号。默认导出也可以放进括号里,下面语句是同样的作用:

import { default as foo, bar } from "mod2.js";

阅读全文 »

ES6 快速上手:生成器

es6-small

生成器又叫“Generators”,是 ES6 的一个重要的新特性。

生成器是一种特殊的函数,下面就是一个生成器:

function* demo(name) {
  yield "Hi " + name + "!";
  yield "Welcome!";
  yield "Goodbye!";
}

调用以后,它返回一个迭代器。里面的 yield 关键字,用来指定每次调用 .next() 所应该返回的值。调用一下:

var iterator = demo("William");

iterator.next(); //{ value: "Hi William!", done: false }
iterator.next(); //{ value: "Welcome!", done: false }
iterator.next(); //{ value: "Goodbye!", done: false }
iterator.next(); //{ value: undefined, done: true }

可以看到,生成器函数的一个特点,就是每次遇到 yield 关键字,函数的执行都会暂停,yield 后面的表达式的值被返回。再调用 .next() ,函数就往下执行,到下一个 yield

生成器也有表达式语法:

let myGenerator = function *(arg) {
  ...
};

生成器的语法要点就两部分:function*yield* 两边允许空格的存在,只要是在 function 和函数名之间就可以。 yield 后面可以跟任意的表达式。

生成器的主要用处是两个:作为迭代器,异步编程。

作为迭代器使用

生成器作为一种函数,它可以成为对象的属性。回想一下《for…of 和迭代器》中所讲的可遍历对象,只要对象拥有一个能返回迭代器的 [Symbol.iterator]() 方法,就能成为一个可遍历对象。既然生成器正好就返回一个迭代器,所以,生成器就是 [Symbol.iterator]() 方法的最佳选择。比如:

var o = {
  items: [1, 2, 3],
  [Symbol.iterator]: function *() {
    for(let i = 0; i < this.items.length; i++) {
      yield this.items[i];
    }
  }
};

用 ES6 新的简写语法,还可以这样写:

var o = {
  items: [1, 2, 3],
  *[Symbol.iterator]() {
    for(let i = 0; i < this.items.length; i++) {
      yield this.items[i];
    }
  }
};

生成器里也可以使用 return 来提前结束遍历:

function *foo() {
    yield 1;
    return;
    yield 2;
    yield 3;
}
let iterator = foo();

iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: undefined, done: true }

迭代器的 .next() 方法还可以传参,比如

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2; // 4 + 2
    yield second + 3; // 5 + 3
}

let iterator = createIterator();

iterator.next(); // { value: 1, done: false }
iterator.next(4); // { value: 6, done: false }"
iterator.next(5); // { value: 8, done: false }"
iterator.next(); // { value: undefined, done: true }

传给 .next() 的参数,会变为上一个 yield 的返回值。因此,第一次调用 .next() 时不要传参数,传了也获取不到。

解析一下上例的执行过程:第一次调用 .next() 时,生成器内部的代码开始执行。第一句是一个赋值语句 let first = yield 1; 自右向左计算,所以先 yield 1,暂停了。第二次调用 .next() 并传入了 4,函数继续执行左边部分 let first = ...,因为 4 成为 yield 的返回值,所以 let first = 4。第二行同理,在 yield first + 2 也就是 yield 6 暂停。第三次调用 .next(5)以后,let second = 5,最后 yield second + 3 的结果是 8。

生成器另一大用处是在异步编程中,因为生成器有暂停的功能。执行一个异步函数时,可以 yield 暂停,等到异步任务完成,再调用 .next() 继续。

yield*

生成器还可以委托,说白了就是,在生成器中调用另一个生成器,例如:

阅读全文 »

ES6 快速上手:for…of 和迭代器

es6-small

for ... of 是 ES6 新增的循环语句,可以遍历一个集合(比如数组),用来代替普通的 for 循环或 forEach。一个例子:

var myArray = ["a", "b", "c"];
for (var value of myArray) {
  console.log(value);
}

那用 for ... of 有什么好处呢?显然用 forEach 也能遍历数组元素,但是 forEach 的缺陷是无法响应 break, continue, return 语句。所以,不能自由跳出,只能循环到底。另外有一个小提示,在大多数浏览器中 forEach 遍历都比 for 循环要慢。虽然 forEach 更简洁,但是从执行效率考虑应当使用 for

然后你会想到用 for...in,但 for...in 也存在一个问题,就是 for...in 给出的索引都是字符串,比如:

var myArray = ["a", "b", "c"];
for (var value in myArray) {
  console.log(value); //"0", "1", "2"
}

如果你直接拿这些索引来做运算,就会出错,比如 value + 10,会得到 "010" 而不是 10。而且,for...in 不光遍历数组内的元素,如果你往数组上添加了自定义的属性,也会被遍历出来。还有一点,最糟糕的情况下,for...in 还可能会不按 0、1、2……的顺序进行遍历。

for...of 可以克服以上这些缺点。它既支持 break, continue, return,同时,索引的类型和原来类型保持一致,对于数组,索引就是 Number 类型。

for...of 也可以遍历类数组对象,比如 NodeList 类型。

还能用来遍历字符串,会把它当作是字符序列:

for (var char of "(⊙ˍ⊙)") {
  alert(char);
}

还支持遍历 MapSet 对象。因为 Map 对象都是键值对,所以,需要使用解构赋值:

for (var [name, age] of persons) {
  console.log(name + "'s age is: " + age);
}

但是 for...of 并不支持遍历普通对象。你可以用 for...in 代替,或者借助 Object.keys

for ( var key of Object.keys(obj) ) {
  console.log(key + ": " + obj[key]);
}

除了上面提到的类型,在其他对象上调用 for...of 会抛出错误,包括nullundefined。为什么?因为 for...of 需要有迭代器的配合。

迭代器(iterators)

“迭代器”:简单来说,迭代器的核心就是一个 .next() 方法,每次调用 .next(),应该返回一个类似下面的对象:

{
  value: 100,
  done: false
}

假设 a[1, 2, 3] 的迭代器,那么每次调用迭代器的 .next(),应该是这样的结果:

a.next(); // { value: 1, done: false }
a.next(); // { value: 2, done: false }
a.next(); // { value: 3, done: false }
a.next(); // { value: undefined, done: true }

可以看出,每次返回的 value 就是从数组中抽出的一个元素,一直遍历到最后,done 表示状态,当它变为 true 时,遍历就结束了。

迭代器的作用就是用来遍历集合,再对里面的数据进行处理,就像我们经常用的 forEach 一样。迭代器的好搭档是 for...of

可遍历对象(iterables)

前面说了,只有在数组、NodeListSetMap、字符串上可以使用for...of,因为它们都是可遍历对象,称为“iterables”。在普通对象上使用 for...of 会抛出错误。

什么是可遍历对象?简单来说,只要对象上拥有一个 [Symbol.iterator]() 方法,该方法返回一个迭代器。那这个对象就是可遍历的,可以用 for...of 进行遍历。

[Symbol.iterator]() 所返回的迭代器又叫默认迭代器(default iterator)。上面所讲的那些类型,都内置了默认迭代器,所以能在 for...of 中使用。

阅读全文 »

ES6 快速上手:Symbol 类型

es6-small

ES5 的时候有六种类型:

  • undefined
  • null
  • Boolean
  • Number
  • String
  • Object

现在,ES6 增加了一种新类型:Symbol。下面是一个创建 symbol 的例子:

var mySymbol = Symbol();
typeof mySymbol; //"symbol"

调用 Symbol 函数就可以创建一个 symbol。因为 symbol 是一个原始类型,并不是对象,所以 new Symbol() 会报错。

symbol 的用处就是作为对象的键。用 symbol 作为对象的键可以避免传统的属性名冲突。比如,用上例的 mySymbol 作为键:

obj[mySymbol] = "ok!"; 

创建 symbol 时,可以传入一个字符串描述:

var sym = Symbol("hi");

即使你每次都传入相同的字符串,返回的 symbol 也互不相等。正因为有这个特性,所以 Symbol 类型可以很好地解决属性名冲突的问题。

什么是冲突呢?当多人合作编码的时候,经常会出现你往对象上加了一个某某属性(比如 $),他人正好也想到了这个名称,当你们同时用了这个名称作为属性,代码之间就会发生冲突,互相覆盖。而用 symbol,即使都用了相同的描述,也不是同一个 symbol。

再列出 Symbol 类型的一些特点:第一,以 symbol 为键的属性,不能用 obj.prop 形式访问,只能用方括号。

第二,for...in 循环、Object.keysObject.getOwnPropertyNames 在遍历一个对象时,都会忽略 symbol 键。

如果你想查看对象上有哪些 symbol 键,可以使用 ES6 的新 API:Object.getOwnPropertySymbols(),返回一个数组,里面都是对象所包含的 symbol。

如果想查看所有属性,用另一个 API:Reflect.ownKeys(obj),会同时返回字符串键和 symbol 键。

第三,in 操作符可以正常工作,比如上例,mySymbol in obj会返回 true。执行 delete obj[mySymbol] 也可以删除这个属性。

第四,symbol 类型不能被自动转换为字符串:

var sym = Symbol("hello");
alert("my symbol is " + sym); // TypeError

可以通过 String(sym) 或调用 sym.toString()

共享 symbol

由于每次调用 Symbol("hello") 返回的 symbol 都是不同的,但有时会有共享同一个 symbol 的需要,所以 ES6 提供了一个全局的 symbol 注册表。

如果你想要共享一个 symbol,用 Symbol.for() 来代替 Symbol()

let sym = Symbol.for("hello");

调用 Symbol.for() 时,如果传入相同的字符串,那么返回的就是同一个 symbol。如果该 symbol 还不存在,则会创建一个 symbol 返回。

Symbol.for() 创建的 symbol 上,还可以调用 Symbol.keyFor() 来查看它的描述。

let sym = Symbol.for("hello");
let sym2 = Symbol.for("hello");
Symbol.keyFor(sym); //"hello"
Symbol.keyFor(sym2); //"hello"

let sym3 = Symbol("hello");
Symbol.keyFor(sym3); // undefined

你可以看到在一个普通 Symbol() 创建的 symbol 上调用 Symbol.keyFor() 会发生什么。

内置 symbol

ES6 内置了一些 symbol,它们都作为 Symbol 函数的属性存在,比如后面关于迭代器文章里要提到的 Symbol.iterator。但是,调用 Object.getOwnPropertySymbols() 时,这些内置 symbol 不被计算在内。

ES6 快速上手:类

es6-small

ES6 终于引入了类。下面是一个类的例子:

class Person {
  constructor(age, country) {
    this._age = age;
    this._country = country;
  }

  get age() {
    return this._age;
  }

  get country() {
    return this._country;
  }

  toString() {
    return `${this.age} ${this.country}`;
  }
}

在上例中,constructor 代表类的构造函数。constructor 里面定义的属性,都是实例属性。定义类时,可以没有 constructor

agecountrytoString 都是原型属性和方法,相当于定义在了 Person.prototype 上。其中 agecountry 是只读的。

如你所见,属性和属性之间,不再需要分隔符了。你可以继续用逗号,或者用分号也可以。

类的语法

第一种是类的声明:

class MyClass {
 ...
}

语法要点:

  • 注意名字后面不能有括号。
  • 类的声明不会被提升到作用域顶部,所以必须先定义后使用。
  • classlet 一样,不能重复声明。

第二种是类表达式:

var MyClass = class {
  constructor() {
    //一个实例属性
    this.name = "demo";
  }
  method() {
    //一个原型方法
  }
}

这种语法下,class 后面也可以再加一个标识符:

var MyClass = class SameClass {
  ...
}

此时 MyClassSameClass 是一样的,可以互相替换。

创建实例还和以前一样:

var instance = new MyClass();

类的一些特点

如果你用 typeof MyClass,会发现其实它还是一个 function

如果没有 new,直接调用 MyClass 会抛出错误。

声明类的代码会全部在严格模式下执行,相当于自动加了 "use strict";

class 里面定义的所有方法都是不可枚举的。也就是说,不能通过 for...in 遍历到,Object.keys 也获取不到。

类也可以当作函数的参数:

function createObj(MyClass) {
  return new MyClass();
}

let obj = createObj(class {
  foo() {
    console.log("hello");
  }
});
obj.foo(); //"hello"

类也可以立即执行,只要在后面多加个括号:

let singleton = new class {
  constructor(foo) {
    this.foo = foo;
  }
  bar() {
    ...
  }
}("hello");

可以用来创建单例(singleton)。

访问器属性

类也可以添加访问器属性,像对象一样。访问器属性就是一对 getter/setter,语法如下:

class Person {
  constructor(name) {
    this.name = name;
  }

  get age() {
    return this.age;
  }
  set age(value) {
    this.age = value;
  }
}

如果省略 setter,属性就是只读的。

静态成员

class Person {
  static walk(a, b) {
     ...
  }
}

静态方法的调用:Person.walk(),其实等同于在 Person 上创建了 walk() 方法。

静态成员默认也是不可枚举的。

阅读全文 »

ES6 快速上手:解构赋值

es6-small

这个特性又叫做“destructuring”。其语法的一般形式为:

[ variable1, variable2, ..., variableN ] = array;

这是数组的解构赋值。后文还有对象的解构赋值。

假设有数组 value = [1, 2, 3],要把三个值分别赋给三个变量 a, b, c,在ES5中要这样写:

var a = value[0];
var b = value[1];
var c = value[0];

ES6 中使用解构赋值:

[a, b, c] = value;

如果需要同时声明变量:

let [a, b, c] = value;

当然换成 var, const 也同样可以。结合“rest”使用:

var [head, ...tail] = [1, 2, 3, 4];
console.log(tail); // [2, 3, 4]

也可以嵌套:

var value = [1, 2, [3, 4, 5]];
var [a, b, [c, d]] = value; //a,b,c,d分别为1,2,3,4

在指定位置省略:

var value = [1, 2, 3, 4, 5];
var [a, , c, , e] = value; //1,3,5

如果出现越界访问的情况,值是 undefined

var value = [1];
var [a, b] = value; //1,undefined

这时候可以指定默认值:

var value = [1];
var [a = 100, b = 2] = value; //1,2

注意的是默认值只对 undefined 值起作用:

var value = [null, null];
var [a = 1, b = 2] = value; //a,b还是null

用处:可以用来交换两个变量的值(没有比这更简单的了):

[el1, el2] = [el2, el1];

或者,从正则表达式的结果里取出分组:

var [, firstName, lastName] = "John Doe".match(/^(w+) (w+)$/);

对象的解构赋值:

var personA = { name: "Paul" };
var personB = { name: "Lily" };

var { name: nameA } = personA;
var { name: nameB } = personB;

console.log(nameA); // "Paul"
console.log(nameB); // "Lily"

对象的 name 属性分别赋给了 nameAnameB。如果用下面这种简洁的写法:

var personA = { name: "Paul" };
var { name } = personA;

那么会把 name 属性赋给同名的变量 name

深层嵌套的对象:

var person = {
  name: {
    firstName: "John", 
    lastName: "Doe"
  }
};
var {name: {firstName, lastName}} = person; //得到两个变量firstName,lastName,注意name未定义

对象里嵌套数组:

var person = {
  dateOfBirth: [1, 1, 1980]
};
var {dateOfBirth: [day, month, year]} = person; //得到三个变量day, month, year

发生嵌套时,一般后面跟了冒号的变量都是未定义的,如上例的 dateOfBirth。这点要注意。

如果要解构的属性不存在,得到的是 undefined。对象解构赋值也可以使用默认值:

var {firstName = "John", lastName: userLastName = "Doe"} = {};

默认值对于 null 同样不起作用。

注意对于下面的写法,会抛出错误:

{ blowUp } = { blowUp: 10 }; // SyntaxError 

因为JS中任何以 { 开始的语句都被解析为一个块语句。解决方案是将整个表达式用一对小括号包裹:

({ blowUp } = { blowUp: 10 });

用处:第一,函数参数的解构:

function myFunc(options) {
  if (options.a) ...
  if (options.b) ...
}

这种语句经常出现在我们的代码中。用 ES6 就看上去更简洁:

function myFunc( {a, b} ) {
  if (a) ...
  if (b) ...
}

阅读全文 »

ES6 快速上手:模板字符串

es6-small

ES6 引入了一种新型的字符串字面量语法,称为模板字符串(template strings)。使用反撇号(`)来代替普通字符串的引号。下面是一个例子:

let greeting = `Hello Jackie`!

有什么用呢?模板字符串为 JavaScript 提供了简单的插值功能(string interpolation)。例:

let name = "Jackie";
let greeting = `Hello ${name}`!

${name} 称为占位符(placeholder)。模板占位符可以是任意 JavaScript 表达式,比如函数调用、算数运算等,都可以作为占位符使用,甚至可以嵌套另一个模板字符串。

如果占位符计算出的值不是字符串,那么会调用 toString 方法。

如果要书写反撇号(`),或者 $,或者 {,都需要转义(加反斜杠)。

与普通字符串的一大区别是,模板字符串可以多行书写:

$("#warning").html(`
  <h1>你好!</h1>
  <p>我叫${name}。这是一个多行
  模板字符串的
  示例。</p>
`);

所有的空格、新行、缩进,都会原样输出在生成的字符串中。

但是,模板字符串并不会自动转义 HTML 字符(比如 < > 等),而且,不支持条件语句、遍历,比其他的模板库还是弱了些。为了突破这些限制,ES6 提供了标签模板(tagged templates)。只需要在起始的反撇号前加上一个额外的标签,如下面的 htmlspecial

var message = htmlspecial`<p>${htmlStr}</p>`;

上面的代码等效于

var message = htmlspecial(templateData, htmlStr);

templateData 是一个不可变数组,存储着模板所有的字符串部分,也就是除去占位符以后剩下的部分,比如上例中 ${htmlStr} 将模板分割为两部分,所以 templateData 数组内就是两个元素,形如 ["<p>", "</p>"]。同时这个数组是不能更改的,相当于用了 Object.freeze

htmlspecial 的一种实现(用来将 < > 等特殊字符转义为实体引用):

function htmlspecial(templateData) {
  var s = templateData[0];
  for (var i = 1; i < arguments.length; i++) {
    var arg = String(arguments[i]);

    // 转义HTML特殊字符。
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // 拼接
    s += templateData[i];
  }
  return s;
}

函数返回的值就会当作模板字符串的输出。

flight