一次性搞清字符编码

正文开始前的絮叨

不看并不影响后面的理解

弄清各种编码的念头在我脑海里面已经浮现过 N 回了,我相信很多人也一样,特别是接触过后端语言的人,会时不时碰到乱码,需要按正确的编码类型解码才能得到正常的文字。一提到编码,我们脑海里可能冒出了 N 多词汇:ASCII 码、Unicode、utf8、gbk、Latin1…他们分别是什么?在什么场合下使用?了解 js/node.js 的小伙伴,对如下两个函数并不陌生:

encodeURI('一'); // "%E4%B8%80"
escape('一'); // "%u4E00"

这两个函数运算的结果分别对应上面提到的哪种“编码”呢?

先说结论

  1. ASCII是给常见符号、字母、数字编码的规则
  2. Unicode是给所有文字编码的规则
  3. utf8是在计算机上对Unicode的实现
  4. gbkLatin1…是各种文字的使用国在Unicode成为世界通用标准之前,自己出的编码规则,一般兼容ASCII

不得不说的编码历史

我们知道,计算机最底层是用二进制表示的,那如何让这些二进制的组合有意义呢?比如我如何表示字母a,如何表示数字1呢?

ASCII 的诞生

计算机的先贤们给常用的书写字符、控制字符都分配了唯一的编号,从 0 到 127,总共 128 个字符,这个编号后面被美国国家标准学会制定成了国家标准,叫ASCII(American Standard Code for Information Interchange),中文翻译叫:美国标准信息交换码(谁叫人家走在计算机发展的前列呢)。所以,我们常见的英文字母也好、数字也好都可以在 ASCII 码表中找到它对应的编号,比如:a的编号十进制是97,数字1的编号十进制是49,标点符号!的编码十进制是33

这样,英语国家就可以愉快的使用计算机处理各种文本了,比如,读取一个文件得到的二进制是:011000010110001001100011,我们知道 8 位为一个字节,我们这里可以分成三个字节:

01100001
01100010
01100011

分别转成十进制就是979899:

parseInt('01100001', 2); // 97
parseInt('01100010', 2); // 98
parseInt('01100011', 2); // 99

查 ASCII 码表我们知道,这个文件的内容就是abc

Latin1,gbk

我们知道,一个字节有 8 个比特位,一个比特位又可以用01表示两种状态,那么,这 8 个比特位都用上的话,理论上来说可以表示 2^8 =256 个字符,而现在 ASCII 码才占用了 128 个,多浪费啊!!后来,欧洲一些使用拉丁字母的国家率先发现了这个“惊天秘密”,在 ASCII 码表的后面继续编码,从 161 开始 255 结束,加入 96 个字母及符号,还搞了个国际化标准名,叫ISO 8859-1,又叫 Latin-1,从此这些国家也可以愉快的使用计算机了。

中国人也不甘示弱,但这剩下的 128 个都不够我写首长诗的,中文汉字实在太多了(汉字总数接近十万,常用汉字也有好几千),一个字节是不够表示的了,至少得用两个字节(2^16 =65536)表示,制定了 GB2312 编码表,及后续的升级版GBK,当然还是得兼容 ASCII 码。(至于在两字节的 gbk 中,如何区分某一字节该识别为 ASCII 码还是和前、后字节组合识别成单独的一个字符,这超出本文的主题了,感兴趣可以去了解下。
比如 gbk 编码得到的十六进制:61d2bb,你怎么解码回去?)

Unicode

部分内容来自刨根究底字符编码之八——Unicode 编码方案概述

在那个各种语言编码独自发育的时代,终于有组织站了出来,着手统一全世界所有文字、符号的编码,这个编码就是Unicode,俗称万国码Unicode为每个字符分配唯一的字符编号(即码点编号、码点值、CodePoint)

Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。目前的Unicode字符分为 17 组编排,每组称为平面(Plane),而每平面拥有 65536 个码位,共 17*65536=1114112 个。然而目前只用了少数平面。

其中第 0 个平面 BMP(Basic Multilingual Plane 基本多语言平面、基本多文种平面、基本平面、平面 0),基本涵盖了当今世界上正在使用中的常用字符。我们平常用到的 Unicode 字符,一般都是位于 BMP 平面上的。BMP 平面以外其他的增补平面(也称为辅助平面)要么用来表示一些非常特殊的字符(比如不常用的象形文字、远古时期的文字等),且多半只有专家在历史和科学领域里才会用到它们;要么被留作扩展之用。目前 Unicode 字符集中尚有大量编号空间未被使用。

另外,BMP 平面有一个专用区(Private Use Zone):0xE000~0xF8FF(十进制 57344~63743),共 6400 个码点,被保留为专用(私用),因而永远不会被分配给任何字符;还有一个被称为代理区(Surrogate Zone)的特殊区域:0xD800-0xDFFF(十进制 55296~57343),共 2048 个码点,目的是用基本平面 BMP 中的两个码点“代理”表示 BMP 以外的其他增补平面的字符(UTF-16 会用到)。

现在倒是齐活了,100w 个字符,理论上是用不完了。然而,需要面临一个新问题了(这个问题 gbk 给绕过去了,还是上面那节最后的问题),0x0000-0x10FFFF这么多字符,必须得用至少4个字节来表示单个字符,比如字符a,若采用Unicode,则需要表示成00000000000000000000000001100001(知道为什么需要这样表示么?其实和 ASCII 码表为什么约定一个字节是 8 位是一样的道理,不约定某种编码需要按多少位作为一个单位解析的话,就会存在解析的多义性,比如你拿到a的二进制01100001,一个个比特位读是一回事,两个比特位读又是另一个意思…只有 8 个比特位一起读才能按 ASCII 码规则读成字符a)。而用 ASCII 码则是:01100001

原来 1 个字节表示的a,现在需要 4 个字节。意味着我原来存储的 ASCII 编码文档,换成 Unicode 编码存储,占用的空间需要翻 4 倍,这哪能忍。

UTF-8 编码

这时候UTF-8跳了出来:我觉得我该出现了。

UTF-8是一种针对Unicode的可变长度字符编码,用 1 到 6 个字节编码Unicode字符。

上面不是说变长编码有多义性么,UTF-8是怎么让计算机识别不同长度的编码呢?

两条规则:

  1. 单字节字符:第一位设置为0后面七位设置为对应的Unicode
  2. n(n>1)字节字符:第一字节的前n位均设置为1n+1位设置为0,后面的字节前两位均设置为10,剩下的二进制位从右至左用这个字符的Unicode码填充,不足的位置填充 0

那么,一个字符该编码为几个字节怎么定呢?

对照如下表:

Unicode 范围 编码格式
U+0000 – U+007F 0xxxxxxx
U+0080 – U+07FF 110xxxxx 10xxxxxx
U+0800 – U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+10000 – U+1FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U+200000 – U+3FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U+4000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

我们拿个字符练练手,汉字:

我们查Unicode 表,知道它的Unicode编码是4E00,当然,如果你熟悉 JavaScript,也可以这样得到:'一'.charCodeAt(0).toString(16)escape('一'),但由于历史原因,这两种方法并不总是可用,比如说这个汉字:\uD842\uDFB7。感兴趣自己试下,然后查找相关资料解惑。

ps: ES6 下提供了"\uD842\uDFB7".codePointAt().toString(16)方法获得准确的 Unicode 码。

扯远了,我们回到手动编码的话题。

对照上面的编码表,我们知道4E00U+0800 - U+FFFF区间,所以其编码格式为1110xxxx 10xxxxxx 10xxxxxx,也就是三个字节,我们将4E00转成二进制,0x4E00.toString(2),得到结果:100111000000000,我们将其与编码格式的xxx位置从右至左对其:

1110xxxx 10xxxxxx 10xxxxxx
     100   111000   000000
填充所有的x位置得到结果:
111001001011100010000000

将其转为 16 进制parseInt('111001001011100010000000',2).toString(16),得到:e4b880即为汉字UTF-8编码了。

验证方式有二:

  1. node.js 安装iconv-lite库,然后:
const iconv = require('iconv-lite');
iconv.encode('一', 'utf8');
// 得到 <Buffer e4 b8 80>
  1. 通过 JavaScript 内置函数encodeURI:
encodeURI('一');
// 得到 %E4%B8%80

BOM

BOM(Byte Order Mark),字节顺序标记,出现在文本文件头部,Unicode 编码标准中用于标识文件是采用哪种格式的编码。

UTF-8一般用EFBBBF作为文件的开头,声明自己的编码类型,一般软件在读取文件的前三个字节的时候便知道了文件的编码类型。但某些编程语言,比如php,在读取文件的时候,会无视BOM,换句话说,它会将这三个字节当做可显示字符去读取,结果就会出现文件开头乱码的情况。

实际上,由于UTF-8编码的特殊性,多读取几个字节,软件是可以猜出当前文档的编码形式的,所以就出现了很多编辑器保存文件时会有UTF-8UFT-8-BOM两种形式的编码方式可选。

与前端相关的编码知识

\数值 deprecated

'\141' === 'a'; // true   \x   x为Unicode码中00~FF范围内字符的8进制编码

\u

'\u4E00' === '一'; // true  \u + Unicode码16进制
'\u{20BB7}' === '\uD842\uDFB7'; // true 超过两个字节的用花括号包围(ES6支持)

\x

'\x61' === 'a'; // true  \x + Unicode码中00~FF范围内字符的16进制编码

\c

仅限于正则表达式中,26 个字母对于开始的 26 个 Unicode 字符

/^\ca$/.test('\u0001'); // true
/^\cb$/.test('\u0002'); // true
//  ...省略中间23个字母组合
/^\cz$/.test('\u001A'); // true

&#x;html 实体

规则&# + 字符 Unicode 码十进制 + ‘;’

字符的 Unicode 十进制为:19968,所以有下面:

<span>&\#19968;</span>
<!-- 二者等同 -->
<span>一</span>

encodeURI/encodeURIComponent

转换U+0000 - U+007F以外的字符为 utf-8 形式:

encodeURIComponent('一'); // "%E4%B8%80"

escape

[a-zA-Z0-9]@*_+-./以外字符的UTF-16码

escape('一'); // %u4E00  => \u4E00
escape('!'); // %21 => \u0021
escape('\uD842\uDFB7'); // %uD842%uDFB7 => \uD842\uDFB7

unescape

解码形如%60%u4E00编码,由于以符号%为单位解析,所以,我们可以通过该函数很容易得到utf8字符串的单字节形式:

var encode = encodeURI('一');
unescape(encode) // "一"

TextDecoder

解码常见编码

const gbkDecoder = new TextDecoder('gbk')
console.log(gbkDecoder.decode(new Uint8Array([0xd2,0xbb]))) // 输出“一”

TextEncoder

转换字符为utf-8字节码

encoder = new TextEncoder()
console.log(encoder.encode('一')) // 输出 Uint8Array(3) [228, 184, 128]

参考文档

  1. 字符编码笔记:ASCII,Unicode 和 UTF-8
  2. 所谓编码–泛谈 ASCII、Unicode、UTF8、UTF16、UCS-2 等编码格式
  3. 刨根究底字符编码
  4. JavaScript character escape sequences

二进制位操作对多状态组合的应用

这其实是属于前辈级程序员们玩剩下的东西,去写win32的程序,调用win32 api的时候,经常会碰到属性状态的组合,比如这个例子

int DisplayResourceNAMessageBox()
{
    int msgboxID = MessageBox(
        NULL,
        (LPCWSTR)L"Resource not available\nDo you want to try again?",
        (LPCWSTR)L"Account Details",
        MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2
    );

    switch (msgboxID)
    {
    case IDCANCEL:
        // TODO: add code
        break;
    case IDTRYAGAIN:
        // TODO: add code
        break;
    case IDCONTINUE:
        // TODO: add code
        break;
    }

    return msgboxID;
}

MessageBox这个函数的第四个参数,用于定义窗体的显示形式,可以是多种形式的组合,就像上面的例子,传入的就是MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2

这种形式的好处就在于,多个属性组合时,不需要为函数定义一大堆的入参,通过巧妙的属性值选取,配合二进制位操作,无论多少个属性,都可以通过一个参数完成。

假如某函数可接受4种状态的组合:a,b,c,d
我们定义四个常量:

const STATUS_A = 1 // 二进制 1
const STATUS_B = 2 // 二进制 10
const STATUS_C = 4 // 二进制 100
const STATUS_D = 8 // 二进制 1000

为什么这么定义? 因为二进制的或运算(|)有如下特征:

1000 | 10 ==> 1010
10 | 101 ==> 111
...

二进制位右对齐后,两个同位置的值有一个为1,则结果为1,否则结果为0。

对于我们的例子:

let status = STATUS_B | STATUS_D // 得到 status 为二进制 1010

很明显STATUS_B对应的第二位与STATUS_D对应的第四位被置为了1,而另两位依然是0

人眼是很容易看明白哪位为0,哪位为1,程序有什么快速的方式知道呢?

答案,依然是位运算,不过这次用与运算(&),特征如下:

101 & 11  ==> 101
111 & 10  ==> 110
...

二进制位右对齐后,两个同位置的值同时为1,则结果为1,其中一项不为1,则结果为0。

利用这个特性,当我们需要知道某状态是否被设置时,只需要入参的值与该状态的值做与运算就好了,因为该状态的值对应位的值肯定为1,所以就有:

let status = STATUS_B | STATUS_D // 得到 status 为二进制 1010

console.log(status & STATUS_A) // 输出0
console.log(status & STATUS_B) // 输出十进制2 对应二进制 10
console.log(status & STATUS_C) // 输出0
console.log(status & STATUS_D) // 输出十进制8 对应二进制 1000

聪明的你,应该已经知道答案了吧。

完整示例:

// 采用parseInt中转一下,直接使用二进制定义更直观
const STATUS_A = parseInt(1,2)
const STATUS_B = parseInt(10,2)
const STATUS_C = parseInt(100,2)
const STATUS_D = parseInt(1000,2)

function statusHandle(statuMask){
    if(statuMask & STATUS_A){
        console.log('状态A被设置')
    }
    if(statuMask & STATUS_B){
        console.log('状态B被设置')
    }
    if(statuMask & STATUS_C){
        console.log('状态C被设置')
    }
    if(statuMask & STATUS_D){
        console.log('状态D被设置')
    }
}
/*      test       */
statusHandle(STATUS_C)
// 状态C被设置
statusHandle(STATUS_A | STATUS_D)
// 状态A被设置
// 状态D被设置

结构化复制

我一直以为的深度复制就是这样:
* for…in循环
* 判断属性是否是自己的
* typeof结果为object还要额外区分具体是普通对象、还是array、还是map…
* 函数、Error无法被深度复制
* 注意循环引用
* JSON API满足大部分需求

昨天看到某篇又是关于深度复制的文章,惯性思维告诉我,不就是深度复制么,写来写去有啥好写的。还好,看在作者是前端大佬的份上,倒是看他能不能把这份冷饭炒出鲜味来。这一看不得了,一个占据大篇幅的新概念冒了出来:结构化克隆算法

具体的介绍直接看MDN,我这里只搬运几种常见的使用结构化克隆方式实现深度拷贝的方法:

  • MessageChannel
function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}
const obj = /* ... */;
const clone = await structuralClone(obj);

特点:异步、兼容性ok

  • History API
function structuralClone(obj) {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}

const obj = /* ... */;
const clone = structuralClone(obj); 

特点:Safari对replaceState调用的限制数量为 30 秒内 100 次。

  • Notification API
function structuralClone(obj) {
  return new Notification('', {data: obj, silent: true}).data;
}

const obj = /* ... */;
const clone = structuralClone(obj);

特点:简洁、兼容性不佳(safari全系列不支持)

当然,由于是whatwg提出的东西,nodejs是沾不上边的,这三个方法都只能用于浏览器端。

绿灯思维

周末看书的时候,书的作者举了个例子,说春节在家的时候与一个亲戚哥哥聊到读书的问题,那个哥哥开门见山提到自己的观点:读书与基因有关。而作者表达的观点是读书这事儿其实与基因没什么太大关系时,引起了那个哥哥的强烈反驳,并试图用他所知道的例子来证明,而并不打算听作者的观点。

对世界的认知,很大程度上影响了人们对于人生意义与价值的看法,进而影响其价值观,最终整个人生的走向也大不一样。

你对世界怎么看,你相信什么,深深影响着你的行动。

怎么改变自己的世界观?我觉得很重要的一点就是要有绿灯思维

什么叫绿灯思维?我们通过一个例子先看看与之相反的更容易理解的红灯思维

上周放假前让同事拟一个xx计划给我,周一按时给我了,但我打开发现每项任务只有工时没有具体的开始日期与结束日期,于是就跑去跟他说,每项任务需要具体的时间节点。他说最开始不是写了一个时间吗,从那个时间开始执行第一项任务,顺着往后推就行了,每完成一项写一个时间。这对我来说显然是不可接受的,我跟他说了我的观点,可他似乎都没在听就开始反驳,你这是多此一举,我这个明明没有问题…

这就是我那位同事红灯思维的体现,遇到与自己观念相悖的观点时,习惯性地就开始反驳。这是一种比较常见的心理现象:习惯性防卫。当我们的观点、思维、一直以来的深刻认知可能受到挑战时,第一时间不是思考对方的挑战与质疑是否合理,而是:有人在反对我的合理性,我需要对峙。这时候,人们的心理就开始亮红灯,习惯性防卫就产生了。

红灯思维的结果,就是把我们自己设置了一个屏障,与外界隔绝开来,过早过滤了大部分信息,让我们只能学到或利用到一小部分外界信息,阻碍了思维的开阔的可能性。久而久之就成了别人眼中的老顽固

看到有人说,红灯思维不就是批判性思考么?仔细想想,其实二者完全不一样:

  • 红灯思维拒绝与自己意见向左的观点
  • 批判性思考是带着质疑的态度去试图接纳别人的观点

二者毫无可比性。

红灯思维弄明白之后,与之相反的绿灯思维就好理解了:以一颗包容的心先认可别人的观点,思考:他的观点可能是对的,最终我也可能同意他的观点,我从哪些角度思考会同意他的观点呢?这个观点的价值在哪里呢?说白了就是首先认可你尚未接受的观点是有价值的,然后站在对方的角度来推演这个观点,最终再决定是否吸纳该观点为自己所有。

绿灯思维的好处不言而喻,我们可以集百家之长,而自成体系。

写这篇文章的目的在于我们如何从红灯心态转变为绿灯心态,这是相当困难的一步,因为很多人在受到别人对自己观点质疑的时候,首先想到的不是别人在质疑他的观点,而是觉得别人在针对他这个人。直接导致了习惯性防卫。

要改变这个情况,就一定要明确”我”与”我的观点/行为结果”是分开的,我的成长来自于”我的观点/行为结果”的改进与持续提升,而别人对于”我的观点/行为结果”提出意见,正是我们从不同角度获得启发和成长的机会。对”我的观点/行为结果”的否定并不是对我本人的否定。

有了这个心理预设之后,就迈进了绿灯思维的大门,多倾听他人的观点、多看书,增加自己接收外界信息的途径,不断更新自己对于世界的认知。

这样,虽然我以为我以为的以为依然还不是我以为的,但我依然将不断成为更好的我。

JavaScript的数据类型

变量名与变量值

var age = 15;

变量名只是标识符,可以理解为内部指针,指向变量值所在的内存地址。变量名没有类型一说。

在一个姓名唯一的星球上,张三用于指代某个人,张三就是变量名,那个人就是变量值。当然,这个人还可以同时拥有多个其它名字:阿三、阿三哥、小三、小三三等。
由于无名之人会挂掉。所以,当把张三、阿三…这些名字都分配给其它人之后,这个人就没有存在的意义了,别人无法描述这个人,也就无法再次给他分配名字(垃圾回收机制)。

后面我们依然会用这个设定来解释变量的引用。

PS:以上仅仅提到了垃圾回收机制中最简单的一种,复杂的,比如:

var o = {};
var arr = [];
arr.push(o);
o = undefined;

由于arr依然持有原来变量o指向的对象,所以就算标识符o不再引用该对象,但该对象依然被标记引用,不能被作为垃圾回收

类型

  • 7 种基本类型
    • null
    • undefined
    • string
    • number
    • boolean
    • symbol
    • bigint
  • 1 种复合类型
    • object

看到这里很多人就会发问了,那么我们用的数组、函数都算什么呢?

ky4eJg.gif

又有机智的小伙伴会问了,那 JSON 又是什么? JSON_互动百科

看很多人的简历写了:精通 JavaScript、JSON 等技术。我就一直在纳闷,JSON 有什么好精通的。

基本类型

基本类型直接代表了最底层的语言实现,具有如下特点:

  • 不可变
  • 无方法/属性

不可变:

var s = 'hello world';
console.log(s[1]); // 输出 e
s[1] = 'a';
console.log(s); // 依然是 hello world

扩展概念:字符串驻留

可以通过内存快照的方式验证如下变量ab是否指向同样的内存地址:

var a = 'hello';
var b = 'hello';

无方法/属性:

初看有些难以理解,毕竟我们经常这样用:

'Hello World'.split(' ');

我们首先要搞明白两个事情:

  • 上面的split方法就是String.prototype.split,验证:
'Hello World'.split === String.prototype.split; // true
  • "Hello World" 并不是 String 的实例,验证:
'Hello World' instanceof String; // false

上面这个例子要铭记,原始类型不能用instanceof判断。

那么为什么可以这样调用:"Hello World".split(" ")

答案呼之欲出:

百度:JavaScript 装箱

Google:JavaScript boxing

所以,在调用"Hello World".split(" ")时,实际调用的是"Hello World"对应的包装类String,实例化后的方法,等同于:

new String('Hello World').split(' ');

如下几种基本类型:stringnumberbooleansymbol都有对于的包装类,分别是StringNumberBooleanSymbol

除了Symbol外,另外三个包装器类均可以被显示实例化,即new Stringnew Numbernew Boolean均合法,但这并不好,而且任何时候都不鼓励显示创建包装器对象,这会模糊原始值与包装器对象的差异。所以在 ES6 中引入的Symbol不让被显示实例化(参考):

new Symbol(); // 报错: Symbol is not a constructor

但如果有别的原因,需要创建symbol的包装器对象,还是有方式的:

var a = Object(Symbol());
typeof a; // object

复合类型

复合类型仅一种object

typeof []; // object
typeof {}; // object
typeof Promise.resolve(); // object
typeof function() {}; // function    咦!我们中出了叛徒

莫慌,尽管在判断函数的时候返回了function,但就算烧成灰,在 JavaScript 中函数依然是属于object类型,一种在底层实现了[[Call]]内部方法的特殊object

复合类型可变,可添加属性/方法,另外一大特点是按引用传值,怎么理解呢?看示例:

var arr = [1, 2, 3];
var arr1 = arr;
arr1.push(4);
console.log(arr1); // [1,2,3,4]
console.log(arr); // [1,2,3,4]

发现异常没?明明我对arr1进行的操作,arr也跟着变了。我们这里不扯高深的内存操作,还是用那外星球的张三来做类比:

var arr = [1, 2, 3]; // 某个人([1,2,3])一出生就被分配了名字:arr(张三)
var arr1 = arr; // 后面他又得到一个名字:阿三(arr1)
arr1.push(4); // 阿三(arr1)被人揍了一顿,毁了容,变成了 [1,2,3,4]
console.log(arr); // 你说张三毁容没毁容?

搞清数据类型的情况下,什么时候是按值传递,什么时候按引用传递是不是一目了然?

大声说出来,以下代码的输出结果:

function fn() {}
fn.hehe = 'hehe';
console.log(fn.hehe); // 输出啥?
function factory(o) {
  o.hehe = 'haha';
}
factory(fn);
console.log(fn.hehe); // 输出啥?

另外,复合类型还有一大特性,不存在两个一样的值:

{} === {} // false
[] === [] // flase
function a(){} === function a(){} // false

类型判断

  • typeof
  • instanceof
  • Object.prototype.toString.call

typeof

有 7 种数据类型,typeof 也返回了 7 个不同的类型值,然鹅这两个 7 并不一一对应:

typeof 123 // number
typeof 'str' // string
typeof true // boolean
typeof Symbol() // symbol
typeof undefined // undefined
typeof fucntion(){} // function
typeof {} // object
typeof null // object
typeof new String("hello") // object

typeof null === 'object'是一开始设计 JavaScript 的时候产生的bug,只能将错就错一直沿用下来,这是客户端语言的无奈(向下兼容)。

所以,当使用typeof判断一个数据是否为object时一定要排除null的情况:

function isObject(obj) {
  return obj && typeof obj === 'object';
}

至于typeof fucntion(){}返回function,是标准规定的。

另外另外另外,还有一种特例

typeof document.all === 'undefined';

至于原因,看看document.all与 IE 的爱恨纠葛

所以,除了null,其他任意的数据我们均可以通过typeof操作符得到其类型。

至于,我们需要知道某对象具体是什么对象,比如:

  • 我们怎么知道变量arr是否是Array的实例?
  • 我们怎么知道变量pro是否是Promise的实例?

那么就要请出instanceof

instanceof

instanceof 顾名思义:a instanceof A的意义就是判断对象a是否是函数(也可以叫类)A的实例。

当然,以上表述不是完全准确,准确的表述应该是:判断A.prototype是否出现在了对象a的原型链上。(这边表述实际还是有问题,见后面)

下面简述__proto__的背景,方便代码演示,会在上面提到的文章中详细阐述。

最早由 Firefox 实现了通过属性__proto__访问对象的原型,这一特性如今几乎被所有的现代浏览器所实现,甚至还写入了 ES6 标准(尽管强烈不推荐在生产环境使用它,而是使用 Object.getPrototypeOf 方法),为方便,我们在演示代码中均用该特性读取原型,比如a.__proto__则是对象a的原型。

回到instanceof

var arr = [];
// 因为,以下表达式的结果为 true
arr.__proto__ === Array.prototype;
// 所以,以下表达式的结果也为 true
arr instanceof Array;

// 同样,由于以下表达式结果为 true
arr.__proto__.__proto__ === Object.prototype;
// 所以,以下表达式的结果也为 true
arr instanceof Object;

使用instanceof去判断对象的子类型有一些弊端,比如:

var o = Object.create(null);
o.__proto__; // undefined
typeof o; // object
o instanceof Object; // false
  • 强行嫁接原型的情况
var obj = {};
obj.__proto__ = Array.prototype;
obj instanceof Array; // true
  • instanceof的行为可被自定义
class MyClass {
  static [Symbol.hasInstance](instance) {
    return true;
  }
}

[] instanceof MyClass; // true

Object.create(null) instanceof MyClass; // true   完全不顾原型了

'sss' instanceof MyClass; // 连基本类型也不管了

var a;
// 理论上来讲,a 是任意值,下面表达式都为 true
a instanceof MyClass; // true
  • bind过的函数无prototype属性,依然可以被实例化
function A(){}
const B = A.bind(null)
const b = new B()
b instanceof A  // true
b.__proto__ === A.prototype // true
b instanceof B // true
b.__proto__ === B.prototype // false

所以,除非你很确定的情况,不然不要轻易使用instanceof去判断一个值是否属于某个类型。

Object.prototype.toString.call

人狠话不多,先看效果:

Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call({}); // [object Object]
Object.prototype.toString.call(''); // [object String]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call(1); // [object Number]
Object.prototype.toString.call(function() {}); // [object Function]
Object.prototype.toString.call(/test/i); // [object RegExp]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(); // [object Undefined]

这些形如:”[object Array]”的值是从哪冒出来的呢?

依据ES6 规范中关于Object.prototype.toString的实现要求,我们翻译成伪代码就一目了然了:

// 以下代码仅表示逻辑,不可作为js代码运行
function toString(val){
  if(val === undefined)return "[object Undefined]"
  if(val === null)return "[object Null]"
  let O = ToObject(val)
  let isArr = IsArray(val)
  ReturnIfAbrupt(isArr) // 如果 isArr 不是一个正常值,比如抛出一个错误,则中断执行
  let builtingTag;
  if(isArr) builtingTag = "Array";
  else if(O 是 exotic String object) builtingTag = "String";
  else if(O 有内部插槽 [[ParameterMap]]) builtingTag = "Arguments";
  else if(O 有内部方法 [[Call]]) builtingTag = "Function";
  else if(O 有内部插槽 [[ErrorData]]) builtingTag = "Error";
  else if(O 有内部插槽 [[BooleanData]]) builtingTag = "Boolean";
  else if(O 有内部插槽 [[NumberData]]) builtingTag = "Number";
  else if(O 有内部插槽 [[DateValue]]) builtingTag = "Date";
  else if(O 有内部插槽 [[RegExpMatcher]]) builtingTag = "RegExp";
  else builtingTag = "Object";
  let tag = O[Symbol.toStringTag];
  ReturnIfAbrupt(tag);
  if(Type(tag) 不是 String)tag = builtingTag
  return `[object ${tag}]`
}

上述伪代码用到了内部方法ToObjectIsArrayTypeReturnIfAbrupt,内部类型String Exotic Objects

特别说明下,很多书籍、博客里面提到的内部属性[[Class]],在 ES6 规范中的Object.prototype.toString不再读取。

通过上面的伪代码,我们可以知道,用户可以控制Object.prototype.toString的返回值:

let o = [];
Object.prototype.toString.call(o); // [object Array]
o[Symbol.toStringTag] = 'MyClass';
Object.prototype.toString.call(o); // [object MyClass]

至此,我们对三种判断类型的方式都已经做了介绍。

但是,你还需要搞清楚,类型判断的初衷是什么?

什么场景下使用什么方法你也需要仔细思量。

鸭子类型(duck typing)

鸭子类型这个概念来自鸭子测试,当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。而在程序设计中:

鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由”当前方法和属性的集合”决定。在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。

鸭子类型在原生的 JavaScript 中最显著的代表莫过于Promise/A+中的thenable对象了。

thenable就是一个特殊的 js 对象,特殊在于这个对象本身或其原型链上存在then方法。所以,你懂的,当你在原型链的顶端添加了then方法,js 下任何的对象都成为了thenable对象:

Object.prototype.then = function(resolve, reject) {};

如上的代码被添加以后,一个正常依赖Promise的项目就无法工作正常了。这是鸭子类型一直被诟病的地方。

但,凡事具有两面性,既然引入这个概念,而且在各大编程语言特别是动态语言中作为一种设计模式,具有不可替代的作用。

依然拿thenable来说,在Promise正式进入规范之前,已经有很多库实现了类似思想,比如QjQuery.Deferred,如何保证已经使用了这些库的既有项目如何快速过度到标准的Promise呢?噢!感谢上帝,这些类库有一个共同特点,就是它们的对象都有then方法,在实现Promise时只需要这样判断一个值是否是thenable

var isThenable = obj =>
  (typeof obj === 'function' || (obj && typeof obj === 'object')) &&
  'then' in obj;

如果用判断鸭子类型的方式来判断一个数组,我们可能需要这样判断:

var obj = {};
var isArr = false;
if('splice' in obj && 'push' in obj && 'pop' in obj....){
  isArr = true;
}

更有甚者,我们可能还需要判断pushpop操作以后,数组的长度是否自动发生了变化…一切的判断取决于你的需求。

你需要知道一个值是否如你所期望的那样,你得去察其外貌观其行为,如果都符合你的预期,你就把它当你所需类型使用,这就是鸭子类型。

无论是使用 js 提供的类型判断,还是使用鸭子类型去判断,在动态语言下面都不存在绝对的靠谱,动态提供了便利的同时,对于开发人员的素质要求也会更高。多人协作的项目、中大型项目、频繁迭代的项目,也许拥抱强类型语言是更稳妥的选择。

个人觉得,从语言层面上来讲,鸭子类型比较像是强类型语言里面的接口(interface),接口的约束性更强,实现一个接口必须实现接口的所有方法与属性,甚至方法的签名都需要一模一样,减少了很多的不确定性。

下面以 typescript 的接口做演示结束本文:

interface Animal {
  name: string;
  say();
}

class Cat implements Animal {
  name = '';
  constructor(name: string) {
    this.name = name;
  }
  say() {
    console.log(`我是猫,我叫${this.name}`);
  }
}

class People implements Animal {
  name = '';
  constructor(name: string) {
    this.name = name;
  }
  say() {
    console.log(`我是人,我叫${this.name}`);
  }
}

function factory(animal: Animal) {
  animal.say();
}

const tom = new Cat('tom');
const zs = new People('张三');

factory(tom);
factory(zs);

// 我是猫,我叫tom
// 我是人,我叫张三

细数JavaScript中一些不能被new的函数

最近发现组内的很多人基础太不牢固,打算整一个关于JavaScript的思维导图,搞一次技术分享。结果发现原来自己也遗漏了相当多得细节。接下来,我就来细数下那些不能被new的函数,这些函数被new的时候会抛出错误:

Uncaught TypeError: xxx is not a constructor

在js中,function类型是一种特殊的对象,特殊在何处呢?特殊在于它是一个在引擎内部实现了[[Call]]方法的对象,所以,在使用typeof取值的时候被特殊对待,返回了function
对于new操作符也是一样,会去查看该对象内部是否实现了[[Construct]]方法,未实现则抛出上述的TypeError

var obj = {
    a(){},
    b: ()=>{},
    c: function* (){},
    d: async ()=>{},
    e: async function d(){},
    f: async function* e(){},
    g: function f(){}
}

以上列出的这些函数中仅仅obj.g可以被实例化。

另外,内置对象上的一些静态方法大多(未验证全部)不可实例化,如:Math.powJSON.parse等。

内置对象的一些实例方法也是同样,如:[].splice"hello".split等。

内置的一些函数也是无法被实例化的,如:encodeURISymbol等。

最后还有Function.prototype,这个特殊的函数也不可被实例化。

ps:
以上提及的这些不能被实例化的函数有一个功能特点(除了Symbol、obj.c、obj.f),那就是它们都没有prototype属性,然鹅,并不能说明一个类能不能被实例化与这个构造函数有没有prototype属性有必然联系,比如:

var A = (function test(){}).bind(this);

bind得到的函数A就不存在prototype属性,但A依然可以被实例化。所以,还是开头的那个结论,一个函数能不能被实例化,就看其内部是否有实现[[Construct]]方法。

当然,话说回来,prototype属性决定了一个可被实例化的函数能否作为父类被继承。还是看这个代码:

var A = (function test(){}).bind(this);
class B extends A{}   // 报错 Uncaught TypeError: Class extends value does not have valid prototype property undefined

A.prototype = null;
class C extends A{}  // 正常

当当当然,null又是个特例,ES6规范规定了,跟在extends后面的只能是一个可被实例化且其prototype属性为对象(包括函数)或null的对象(包括函数),所以下面的代码是合法的:

class D extends null{}

但是这样声明的D无法被实例化,因为找不到超类的构造函数,需要这样处理:

class D extends null{
    constructor(){
        return Object.create(new.target.prototype)
    }
    otherMethod(){}
}

Dart学习笔记:Iterable与Stream

避免烂尾,先开坑。

初略看了下Stream API,应该是一个很深的坑。Stream实现了类似于Rx的许多API,估计过年在家要好好读读文档了。


本打算只写关于Stream的东西,后面翻了一下文档,发现StreamIterable的API大致相同,唯一的区别在于Iterable是同步的,而Stream是异步的,它们的定义形式,类似于JavaScript下的生成器,而实际上,在Dart中,也将二者统称为生成器。

  • 定义Iterable
Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}
  • 定义Stream
Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}
  • 递归生成器
Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

Dart学习笔记:Future

Dart下的Future类似于ES6下新增的Promise,也是为了解决异步回调带来的各种问题。

构造函数

Future(FutureOr<T> computation())

computation 的返回值可以是普通值或者是Future对象

Future<num> future1 = Future((){
    print('async call1');
    return 123;
});

Future<Future> future2 = Future((){
    print('async call2');
    return future1;
});

需要注意的是,computation函数体中的代码是被异步执行的,这与Promise构造函数的回调执行时机是不一样的,如需要被同步执行,则使用如下这个命名构造函数:

Future.sync(FutureOr<T> computation())

Future<num> future = Future.sync((){
    print('sync call');
    return 123;
});

Future.delayed(Duration duration, [ FutureOr<T> computation() ])

延时后再执行computation

Future.delayed(Duration(seconds: 1), () {
    print("print after 1 second");
});

Future.value([FutureOr<T> value ])

创建一个future对象,以value完成

Future.value(123);

Future.error(Object error, [ StackTrace stackTrace ])

创建一个future对象,以错误状态完成

Future.error('some error');

Future.microtask(FutureOr<T> computation())

dart下的异步任务队列有两个:event queuemicrotask queuemicrotask queue的优先级更高,而future的任务默认是属于event queue。上面这个构造函数就可以创建属于microtask queue的future。

Future.microtask(()=>print("microtask"));

实例方法

then<R>(FutureOr<R> onValue(T value), { Function onError }) → Future<R>

行为与ES6中Promise.prototype.then几乎一致

Future.val(123).then((val)=>print(val));

Future.error("some error").then((val) {},{onError: (err) => print(err)});

catchError(Function onError, { bool test(Object error) }) → Future<T>

处理future中的异常,第二个参数不使用的情况下,与ES6中的Promise.prototype.catch一致

Future.error("some error").catchError((err) => print(err));

第二个参数提供的情况下,必须返回true,回调函数才能正常捕获错误。暂没想到该参数有什么用。

asStream() → Stream<T>

创建流,该流包含future中的值或错误

timeout(Duration timeLimit, { FutureOr<T> onTimeout() }) → Future<T>

给future指定一个超时时间,若超时了执行onTimeout回调,并返回新的值作为下一个future的值

  Future.delayed(Duration(seconds: 2)).timeout(Duration(seconds: 1),
      onTimeout: () {
    print("timeout");
    return "TIME OUT";
  });

whenComplete(FutureOr action()) → Future<T>

类似于ES7中的Promise.prototype.finally,无论future是正常完成还是产生异常都会执行

Future.value("value").whenComplete(() {
    print('complete');
  })

静态方法

any<T>(Iterable<Future<T>> futures) → Future<T>

行为与ES6中Promise.race一致,若futures为空,则返回的future永远不会完成

Future.any([
  Future.delayed(Duration(seconds: 1)).then((val) => 1),
  Future.delayed(Duration(seconds: 2)).then((val) => 2),
  Future.delayed(Duration(seconds: 3)).then((val) => 3),
]).then((val) {
  print(val); // val == 1
});

doWhile(FutureOr<bool> action()) → Future

do{}while()的异步版本

var i = 0;
Future.doWhile(() {
  print(i++);
  return Future.value(i < 10);
}).then((val) => print('after doWhile'));

forEach<T>(Iterable<T> elements, FutureOr action(T element)) → Future

异步forEach,future会一直等待elements遍历完毕才会变成完成状态,除非在遍历过程中出现错误

Future.forEach([1, Future.delayed(Duration(seconds: 2)), 3], (val) {
  print(val);
  return val;
}).then((val) => print('success=>>>$val'));

wait<T>(Iterable<Future<T>> futures, { bool eagerError: false, void cleanUp(T successValue) }) → Future<List<T>>

eagerError参数未true的情况下,其行为与ES6中Promise.all基本一致

Future.wait([
  Future.delayed(Duration(seconds: 1)).then((val) => 1),
  Future.delayed(Duration(seconds: 2)).then((val) => 2),
  Future.delayed(Duration(seconds: 3)).then((val) => 3),
]).then((val) => print(val));  // 3s后得到val 为 [1,2,3]

eagerError默认值为false,表明futures中任何一项执行出错都不会立即返回新的future,而是需要等待所有项都是完成状态才会返回。

举例来讲,以下代码在eagerErrorfalse的情况下,需要经历3s才会输出err=>error,而设置eagerErrortrue,则只需要1.5s:

Future.wait([
  Future.delayed(Duration(milliseconds: 1500))
      .then((val) => Future.error('error')),
  Future.delayed(Duration(seconds: 1)).then((val) => 1),
  Future.delayed(Duration(seconds: 2)).then((val) => 2),
  Future.delayed(Duration(seconds: 3)).then((val) => 3),
], eagerError: false)
    .then((val) => print('then=>$val'), onError: (err) => print('err=>$err'));

cleanUp在futures中某项出错的时候,会给每项正常执行的future提供清理操作,传递给cleanUp的参数为每个正常执行项的完成值,Future.await只会处理最先抛出的错误,但是整个程序会等待所有的future项完成才会结束。

Future.wait([
  Future.delayed(Duration(milliseconds: 1500))
      .then((val) => Future.error('error')),
  Future.delayed(Duration(seconds: 1)).then((val) => 1),
  Future.delayed(Duration(seconds: 2)).then((val) => 2),
], cleanUp: (val) => print('complete=>${val}'))
    .then((val) => print('then=>$val'), onError: (err) => print('err=>$err'));

以上代码会在1.5s后依次输出:

complete=>1
complete=>2
err=>error

粗略过完了所有的API,应该还有很多细节遗漏的地方,特别是在内部任务调度这块,涉及到代码执行顺序的问题,需要在实际项目中去摸索、加深。

Dart学习笔记:异常

Dart 和 Java 不同的是,所有的 Dart 异常是非检查异常。 方法不声明他们可能抛出的异常, 并且你不被要求捕获任何异常。

Dart 提供了 ExceptionError 类型, 以及许多预定义的子类型。你还可以定义自己的异常类型。

Throw

Dart 不仅仅可以抛出Exception或者Error对象,还可以抛出任何非null对象为异常。

throw new FormatException('Expected at least 1 section');

throw 'Out of llamas!';

Catch

捕获异常可以避免异常继续传递(你重新抛出异常除外)。 捕获异常给你一个处理该异常的机会。

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  buyMoreLlamas();
}

对于可以抛出多种类型异常的代码,你可以指定多个捕获语句。每个语句分别对应一个异常类型, 如果捕获语句没有指定异常类型,则该可以捕获任何异常类型:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
// e为异常对象,s为调用堆栈信息
} on Exception catch (e, s) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

使用 rethrow 关键字可以把捕获的异常重新抛出。

final foo = '';

void misbehave() {
  try {
    foo = "You can't change a final variable's value.";
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}

Finally

要确保某些代码执行,不管有没有出现异常都需要执行,可以使用 一个 finally 语句来实现。如果没有 catch 语句来捕获异常, 则在执行完 finally 语句后, 异常还是会被抛出。

try {
  breedMoreLlamas();
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

try {
  breedMoreLlamas();
} catch(e) {
  print('Error: $e');  // Handle the exception first.
} finally {
  cleanLlamaStalls();  // Then clean up.
}

Dart学习笔记:类

本想按《Dart学习笔记 1: xxx》格式的,但这样就相当于立了flag,后面还需要有2、3、4…还是算了吧,指不定就烂尾了。
今天刚好又回过头来瞄了下官方文档,顺便记下笔记。

声明

class Point {
  num x; // 成员变量x,初始值null.
  num y = 0; // 成员变量y,初始化为0.
  num _z = 1; // 成员变量_z, 初始化为1,仅在库内可访问
}

实例化

Point p = new Point();
// dart2.0 可以省略 new关键字
Point p1 = Point();

构造函数

几种类型的构造函数:
1. 未定义构造函数的时候,会自动生成一个无参的默认构造函数,并且会调用超类的没有参数的构造函数。
2. 普通构造函数:定义一个和类名字一样的方法就定义了一个构造函数

// 常规形式
class Point {
  num x;
  num y;
  Point(num x, num y) {
    // 如果不存在同名冲突,this可以省略
    this.x = x;
    this.y = y;
  }
}
// 语法糖形式
class Point {
  num x;
  num y;
  Point(this.x, this.y);
}
  1. 命名构造函数

使用命名构造函数可以为一个类实现多个构造函数, 或者使用命名构造函数来更清晰的表明你的意图。

class Point {
  num x;
  num y;
  Point.fromJson(Map json) {
    x = json['x'];
    y = json['y'];
  }
}
  1. 常量构造函数

如果你的类提供一个状态不变的对象,你可以把这些对象 定义为编译时常量。要实现这个功能,需要定义一个 const 构造函数, 并且声明所有类的变量为 final。

class ImmutablePoint {
  final num x;
  final num y;
  const ImmutablePoint(this.x, this.y);
  static final ImmutablePoint origin =
      const ImmutablePoint(0, 0);
}
  1. 工厂方法构造函数

如果一个构造函数并不总是返回一个新的对象,则使用 factory 来定义 这个构造函数。例如,一个工厂构造函数 可能从缓存中获取一个实例并返回,或者 返回一个子类型的实例。

class Logger {
  final String name;
  bool mute = false;

  // _cache 是库内私有
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final logger = new Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }
  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) {
      print(msg);
    }
  }
}
// 使用的时候和其它构造函数一样
var logger = Logger('UI');
logger.log('Button clicked');

重定向构造函数

有时候一个构造函数会调动类中的其他构造函数。 一个重定向构造函数是没有代码的,在构造函数声明后,使用冒号调用其他构造函数。

class Point {
  num x;
  num y;
  Point(this.x, this.y);
  Point.alongXAxis(num x) : this(x, 0);
}

初始化列表

在构造函数体执行之前除了可以调用超类构造函数之外,还可以 初始化实例参数。 使用逗号分隔初始化表达式。

class Point {
  final num x;
  final num y;
  final num distanceFromOrigin;

  Point(x, y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y){
            print('In Point.fromJson(): ($x, $y)');
        }
}

调用超类构造函数

class Person {
  String firstName;
  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person类没有默认构造函数,必须手动调用super.fromJson(data);
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

方法

import 'dart:math';

class Point {
  num x;
  num y;
  Point(this.x, this.y);

  num distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

Getters and Setters

Getters 和 Setters 是用来设置和访问对象属性的特殊函数。每个实例变量都隐含的具有一个 getter, 如果变量不是 final 的则还有一个 setter。 你可以通过实现 getter 和 setter 来创建新的属性, 使用 get 和 set 关键字定义 getter 和 setter。getter 和 setter 的好处是,我们在代码中一开始使用实例变量,后来,把实例变量用函数包裹起来,而使用实例变量的地方不需要修改。

class Rectangle {
  num left;
  num top;
  num width;
  num height;

  Rectangle(this.left, this.top, this.width, this.height);

  num get right             => left + width;
      set right(num value)  => left = value - width;
  num get bottom            => top + height;
      set bottom(num value) => top = value - height;
}

抽象类

使用 abstract 修饰符定义一个 抽象类—一个不能被实例化的类。 抽象类通常用来定义接口, 以及部分实现。抽象类通常具有抽象函数。

abstract class AbstractContainer {
  void updateChildren(); // 抽象方法
}

隐式接口

dart已经移除了interface关键字,也就是说,在dart中不存在如java或ts中那样明确定义接口的语法,取而代之的是每个类都隐式的定义了一个包含所有实例成员的接口, 并且这个类自己实现了这些接口,当然,由于dart同时也不支持成员函数的overload,所以,实现接口的时候,函数签名必须与接口定义的一致

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// Impostor必须实现Person的所有成员函数及变量.
class Impostor implements Person {
  // 尽管不会用到_name,也必须定义该成员变量
  get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

实现多个接口:

class Point implements Comparable, Location {...}

继承

继承就跟es6差不多了,使用extends关键字来定义子类,并可以在子类中通过super关键字来引用超类(所有祖宗类)

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}

重写方法

可以使用 @override 注解来表明你的函数是想重写超类的一个函数

class SmartTelevision extends Television {
  @override
  void turnOn() {...}
  // ···
}

重写操作符

这些操作符可以被覆写。 例如,如果你定义了一个 Vector 类, 你可以定义一个 + 函数来实现两个向量相加。

class Vector {
  final int x;
  final int y;
  const Vector(this.x, this.y);

  /// 覆写 + (a + b).
  Vector operator +(Vector v) {
    return new Vector(x + v.x, y + v.y);
  }

  /// 覆写 - (a - b).
  Vector operator -(Vector v) {
    return new Vector(x - v.x, y - v.y);
  }
}

类的Mixins

“我们需要一种在多个类层次结构中重用类的代码的方法”,大白话就是当我们想要在不共享相同类层次结构的多个类之间共享行为时,或者在超类中实现此类行为没有意义时,Mixins非常有用。

先看定义mixin的方式:
1. 通过类隐式定义(不能有构造函数)

class Walker {
  void walk() {
    print("I'm walking");
  }
}
  1. 通过抽象类定义
abstract class Walker {
  // This class is intended to be used as a mixin, and should not be extended directly.
  factory Walker._() => null;
  void walk() {
    print("I'm walking");
  }
}
  1. 通过mixin关键字定义
mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

使用的时候,通过with关键字,后面跟一个或多个mixin

class Cat extends Mammal with Walker {}
class Dove extends Bird with Walker, Flyer {}

静态成员

使用static关键字来实现类级别的变量和函数。

class Queue {
  static const initialCapacity = 16;
  // ···
}

class Point {
  num x;
  num y;
  Point(this.x, this.y);
  // 由于静态方法不是通过实例访问,所以其内部无法使用this
  static num distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

一个符合Promise/A+的Promise实现

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

const stateSymbol = Symbol('state');
const valueSymbol = Symbol('value');
const innerPropsSymbol = Symbol('innerProps');

const isFunction = val => typeof val === 'function';
const isObject = val => val && typeof val === 'object';
const isThenable = val => (isFunction(val) || isObject(val)) && 'then' in val;
const nextTick = fn => setTimeout(fn);

function handleTasks(ctx) {
  const {
    [innerPropsSymbol]: { tasks },
    [stateSymbol]: state,
    [valueSymbol]: value,
  } = ctx;
  nextTick(() => {
    if (state === REJECTED) {
      if (ctx[innerPropsSymbol].haveUnhandleReject && tasks.length === 0) {
        console.error('未处理reject');
      } else {
        ctx[innerPropsSymbol].haveUnhandleReject = false;
      }
    }
    while (tasks.length) {
      handleTask(tasks.shift(), state, value);
    }
  });
}

function handleTask(task, state, value) {
  const { onFulfilled, onRejected, resolve, reject } = task;
  try {
    if (state === RESOLVED) {
      isFunction(onFulfilled) ? resolve(onFulfilled(value)) : resolve(value);
    } else if (state === REJECTED) {
      isFunction(onRejected) ? resolve(onRejected(value)) : reject(value);
    }
  } catch (err) {
    reject(err);
  }
}

function MyPromise(fn) {
  if (!this instanceof MyPromise) throw new Error('只能用于构造函数');
  if (!isFunction(fn)) throw new TypeError('MyPromise的参数只能是函数');

  this[stateSymbol] = PENDING;
  this[innerPropsSymbol] = {
    tasks: [],
    haveUnhandleReject: true,
  };
  this[valueSymbol] = undefined;

  const transValue = (state, value) => {
    if (this[stateSymbol] !== PENDING) return;
    this[valueSymbol] = value;
    this[stateSymbol] = state;
    handleTasks(this);
  };

  const onFulfilled = value => transValue(RESOLVED, value);
  const onRejected = reason => transValue(REJECTED, reason);
  // 由于resolve的过程可能存在异步改变状态的情况
  // 所以,需要确保resolve或reject只能有一个被调用,且只调用一次
  let ignor = false;
  const resolve = value => {
    if (ignor) return;
    ignor = true;
    if (value === this) {
      onRejected(new TypeError('promise循环调用错误'));
    } else if (value instanceof MyPromise) {
      value.then(onFulfilled, onRejected);
    } else if (isThenable(value)) {
      try {
        const then = value.then;
        if (isFunction(then)) {
          new MyPromise(then.bind(value)).then(onFulfilled, onRejected);
        } else {
          onFulfilled(value);
        }
      } catch (err) {
        onRejected(err);
      }
    } else {
      onFulfilled(value);
    }
  };

  const reject = reason => {
    if (ignor) return;
    ignor = true;
    onRejected(reason);
  };

  try {
    fn(resolve, reject);
  } catch (err) {
    reject(err);
  }
}

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  return new MyPromise((resolve, reject) => {
    this[innerPropsSymbol].tasks.push({
      onFulfilled,
      onRejected,
      resolve,
      reject,
    });
    // new Promise(resolve=>resolve()).then(fn1,fn2) 过程中
    // 状态改变在前,push task在后,所以,这里也触发一次任务队列处理
    // 或者为已经不在pending状态的promise添加then,也需要触发任务处理
    if (this[stateSymbol] !== PENDING) {
      handleTasks(this);
    }
  });
};

// 两个重要的工具函数

MyPromise.resolve = function(value) {
    // fix on 2019.02.12
    // Promise.resolve的参数为Promise时,直接返其本身
  if(value instanceof MyPromise)return value;
  return new MyPromise(resolve => resolve(value));
};

MyPromise.reject = function(reason) {
  return new MyPromise((_, reject) => reject(reason));
};

标准文档中文翻译:Promise A+

其他方式的nextTick

这几天利用空余时间在完成一个去年未尽的活:从零开始实现一个Promise。

浏览器中原生的Promise,then里面的回调被放入事件循环中的微任务队列,而setTimeout的回调则被放入了宏任务队列。一轮事件循环结束,先清空微任务队列,才会执行一个宏任务。所以,Promise中then的回调执行时机早于setTimeou的回调执行时机的。

我们既然打算自己实现一个Promise的话,那么它的执行时机越早越好,从目前我所知的各种回调来说,没有什么比Promise更早了,那么存不存在一个比setTimeout执行时机更早的呢?

好在,原来看司徒正美的博客有留意到他提过某些标签的onerror事件执行会比较早,具体记不得是哪篇博客了,于是乎自己尝试一番得到了下面这个函数:

function nextTick(fn){
    var img = document.createElement('img');
    img.onerror = function(){fn()}
    img.src='data:;,'
}

测试:


setTimeout(function(){console.log(1)}) nextTick(function(){console.log(2)});

在IE9+及其他现代浏览器上得到的输出顺序都是:2、1

创建一个Dom元素而不插入文档流,代价应该不是很大,如果实在要纠结这个,我们不如把它放入一个闭包中,达到重复利用:

var nextTick = (function(){
    var img = document.createElement('img');
    return function nextTick(fn){
        img.onerror = function(){fn()}
        img.src='data:;,'
    }
})()

Vue中的$nextTick实现曾经用过MessageChannel,不过后面取消了,大部分情况下使用微任务

var nextTick = (function(){
    const channel = new MessageChannel()
    return function nextTick(fn){
        channel.port1.onmessage = ()=>fn()
        channel.port2.postMessage('')
    }
})()

另外,在IE10、IE11及Edge上有一个非标准的函数:setImmediateVue中的nextTick实现也会优先使用它,但我实验发现,它与setTimeou的执行顺序是无法确定。

另外另外另外再提一句,MutationObserver的回调和Promise一样,也属于micro-task,而且他的执行时机更是早于Promise。以下是实现:

function  nextTick(fn){
    var div = document.createElement('div');
    new MutationObserver(fn).observe(div, {  attributes: true })
    div.setAttribute('data-test','test')
}

DFS与BFS

DFS与BFS实现,考虑到递归有爆栈的可能,所以,采用循环来做,实现思路都是通过维护一个stack,只是入栈的规则不一样。
以遍历DOM节点为例:


function dfs(node) { const stack = [node]; const nodes = []; let tmp; while (tmp = stack.pop()) { nodes.push(tmp) let childs = tmp.children, len = childs.length, i = len - 1; for (; i > -1; i--) { stack.push(childs[i]) } } return nodes }
function bfs(node) {
    const stack = [node];
    const nodes = [];
    let tmp;
    while (tmp = stack.pop()) {
        nodes.push(tmp)
        let childs = tmp.children, len = childs.length, i = 0;
        for (; i < len; i++) {
            stack.unshift(childs[i])
        }
    }
    return nodes
}

微信笔试题:LazyMan实现

实现一个函数LazyMan,执行情况如下:

LazyMan("Hank")
// 输出:Hi! This is Hank!
 
LazyMan("Hank").sleep(10).eat("dinner")
// 输出 Hi! This is Hank!
// 等待10秒..
// 输出 Wake up after 10
// 输出 Eat dinner
 
LazyMan("Hank").eat("dinner").eat("supper")
// 输出 Hi This is Hank!
// 输出 Eat dinner
// 输出 Eat supper
 
LazyMan("Hank").sleepFirst(5).eat("supper")
// 等待5秒
// 输出 Hi This is Hank!
// 输出 Eat supper

在有Promise的情况下,还是很好做的:

function LazyMan(name) {
  function sleep(time) {
    return new Promise(resolve => setTimeout(resolve, time * 1000));
  }

  function addToQueue(queue, item, addToHead) {
    queue[addToHead ? 'unshift' : 'push'](item);
  }

  function resolvePromiseQueue(queue) {
    queue.reduce((last, cur) => {
      return last.then(cur);
    }, Promise.resolve());
  }

  const QUEUE_NAME = Symbol('QueueName'); // 通过Symbol ,使queue成为“私有”成员

  return new class LazyMan {
    constructor() {
      this[QUEUE_NAME] = [() => console.log(`Hi! This is ${name}!`)];
      Promise.resolve().then(resolvePromiseQueue.bind(null, this[QUEUE_NAME]));
    }
    eat(food) {
      addToQueue(this[QUEUE_NAME], () => console.log(`Eat ${food}`));
      return this;
    }
    sleep(time, sleepFirst) {
      addToQueue(
        this[QUEUE_NAME],
        () => sleep(time).then(() => console.log(`Wake up after ${time}`)),
        sleepFirst
      );
      return this;
    }
    sleepFirst(time) {
      this.sleep(time, true);
      return this;
    }
  }();
}

Cookie那些事儿

面试别人的时候,经常碰到自诩精通HTTP协议的人,我一般会丢个问题“HTTP协议是否有状态?”,来试探是否真的“了解”HTTP协议,很多人听到问题就懵逼了,这TM还是我认识的HTTP协议么。偶尔碰到一个说是无状态的,我继续追问“既然HTTP是无状态的,那么我打开一个网站,然后刷新一下,服务器能否知道这次的访问者和上次的访问者是否是同一个客户端?”,然后继续懵逼,转而把答案改为“那应该是有状态的吧?”

所以啊,我们很多时候学技术性东西,还是要追求甚解,知其然,知其所以然。

HTTP协议当然是无状态的,我们在实际场景中是如何区分客户的呢?传统的做法就是利用Cookie来标记用户,用户第一次访问服务器,服务器便在Response Header中添加Set-Cookie,写入标记该用户的唯一标识符,用户二次访问时,将该标识符带回,服务器即可区分该用户是否是之前的用户了。

说到我们本文的主角Cookie,当然不是那么简简单单的事情(事实上是我自己一直以来将其看得过于简单,后来脸被打肿)。

两种方式设置cookie:

  • Set-Cookie(http方式)
    服务端通过在Response Header中添加Set-Cookie,告知客户端如何存储cookie,一般格式:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>;Max-Age=<non-zero-digit>;Expires=<date>;Path=<path-value>;SameSite=<Strict|Lax>; Secure; HttpOnly
  • document.cookie(代码方式)
    客户端(通常指浏览器)通过javascript直接给document.cookie赋值设置cookie
document.cookie="<cookie-name>=<cookie-value>; Domain=<domain-value>;Max-Age=<non-zero-digit>;Expires=<date>;Path=<path-value>;SameSite=<Strict|Lax>; Secure;"

很明显,除了客户端无法设置HttpOnly外,其他与服务端基本一致。

另外,需要注意的是,书写顺序问题:<cookie-name>=<cookie-value>;必须写在最前面,后面的其他项顺序则可以随意了。这样也就限制了一句设置代码只能设置一个cookie,服务端需要在一次http响应中添加多个set-cookie header来一次性写入多个cookie,客户端则需要调用多次document.cookie赋值才能设置多个cookie。

cookie-name=cookie-value

根据RFC6265,二者可取值情况如下:
– cookie-name 区分大小写,字母、数字以及!#$%&’*+-.^_|`~。
– cookie-value 是可选的。支持字母、数字及!#$%&'()*+-./:<=>?@[]^_{|}`~。关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 cookie-value 所允许使用的字符的要求是有用的。

尽管目前实验了Chrome及Firefox支持更多的字符串,甚至中文,但由于各浏览器、各服务端解析可能存在不一致的情况,还是推荐按标准形式,可以减少古怪问题出现的概率。

  • __Secure-前缀,以 __Secure- 为前缀的 cookie(其中连接符是前缀的一部分),必须与 secure 属性一同设置,同时必须应用于安全页面(即使用 HTTPS 访问的页面)。
  • __Host-前缀,以 __Host- 为前缀的 cookie,必须与 secure 属性一同设置,必须应用于安全页面(即使用 HTTPS 访问的页面),必须不能设置 domain 属性 (也就不会发送给子域),同时 path 属性的值必须为“/”。

__Secure-前缀为例讲解其如何弥补后面即将提到的Secure的不足之处:
由于发往服务端的Cookie只有key-value对,并不包含domain、secure之类的其它信息。当我们用token作为用户标识时,设置domain为www.a.comcookie,并带上了Secure设置,尽管www.a.com为https,但攻击者(中间人)完全可以构造一个非https的网站xxx.a.com诱导用户访问(被劫持的网站,不需要真实存在),然后设置domain为.a.com的cookie,当服务端拿到这两个token时并没法分别哪个是安全的token。
但是我们若不以token作为cookie name,而是使用__Secure-token,攻击者就无计可施了,因为设置__Secure-前缀的cookie必须同时设置Secure,而Secure的设置只能在https链接中,攻击者又无法通过中间人的方式攻击https。

Domain=domain-value

用于设置cookie生效的范围。若不设置该项,浏览器默认将其标记为HostOnly,也就是只有在完全匹配的hostname(注意是hostname,而不是host,也就是说cookie的domain不区分端口)下面方可读取cookie。比如,我在a.com下面设置cookie时没有指定domain,那么我在sub.a.com下面就无法读取到该cookie。

若设置domain,domain的取值只能是当前hostname或父级域,比如,在sub.a.com下面,可选的domain只能是sub.a.com或者a.com,其它取值将会被浏览器忽略。

另外,非常值得注意的是,一些顶级域本来就有两段或多段,比如:.com.cn.usa.gov.edu.cn等等,我们就无法在形如a.com.cn的网站中设置domain为.com.cn的cookie。

除了上面提到的多段顶级域名,一些提供网站服务的第三方平台,比如github.io、sina sae等,提供子域名给各个用户,也就是说w3c.github.ioalibaba.github.io不是同一个网站,那么在w3c.github.io下就不能写domain为.github.io的cookie。

由于上面提到的两种情况存在,Mozilla很早之前就为此建立了一个列表Public Suffix List,专门用于维护顶级域名及类似于github.io这样的第三方网站提供商的域名。据Public Suffix List官网介绍,目前使用该列表的软件包括Firefox、Chrome、IE等主流浏览器及其他一些对域名查询有需求的软件。
如果希望自己的网站添加进该列表,可以来这里提PR(被通过应该需要很多手续)。

Path=path-value

指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部。字符 %x2F (“/”) 可以解释为文件目录分隔符,此目录的下级目录也满足匹配的条件(例如,如果 path=/docs,那么 “/docs”, “/docs/Web/” 或者 “/docs/Web/HTTP” 都满足匹配的条件)。
另外,由于domain与path是分开解析的,所以a=1;domain=.a.com;path=/x/y,也能在sub.a.com/x/y下被读取。

Expires=date

date应该是符合<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT格式的字符串,用于标识cookie的过期时间。若不设置该项(下面的max-age也不设置)则cookie是会话级的,浏览器关闭则清除该cookie。

Max-Age=non-zero-digit

non-zero-digit是只在 cookie 失效之前需要经过的秒数。一位或多位非零(1-9)数字(ps:其实0也可以,只是没意义,浏览器可以实现为直接忽略)。一些老的浏览器(ie6、ie7 和 ie8)不支持这个属性。对于其他浏览器来说,假如二者 (指 Expires 和Max-Age) 均存在,那么 Max-Age 优先级更高。

SameSite=Strict|Lax|Unset(默认)

允许服务器设定一则 cookie 不随着跨域请求一起发送,这样可以在一定程度上防范跨站请求伪造攻击(CSRF)。
– Strict 任何时候都不跨域发送cookie,所以当你发现从a网站进入b网站,b网站的登陆状态总是失效的,然后刷新一下又正常了,不要悲伤、不要诧异,检查下你用于标记登陆状态的cookie是不是设置了samesite为strict
– Lax 点击a标签、form的get请求、通过location改变、通过window.open方式打开时会携带cookie,而ajax、script、link、img、iframe、form的post发起的跨域请求则不会携带cookie
– Unset 默认值,任何时候都会跨域携带cookie
samesite目前属于实验性属性,还未进入标准,目前(2018.10.08)兼容性不是太乐观can i use

Secure

一个带有安全属性的 cookie 只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器,同时无法在非https的页面通过document.cookie读取,这可以有效防范SSl strip后cookie失窃。然而,保密或敏感信息永远不要在 HTTP cookie 中存储或传输,因为整个机制从本质上来说都是不安全的,比如前述协议并不意味着所有的信息都是经过加密的。
新版Chrome与Firefox已经不支持在非https的链接中设置Secure了。

HttpOnly

带此标识的cookie只能以http的方式设置于读取,也就是说,通过document.cookie的方式既不能写入也不能读取带HttpOnly标识的cookie,可有效防止xss的方式窃取cookie。事实上,一般后端web框架的sessionid一般都是设置了HttpOnly的。

第三方Cookie

跨域设置cookie的问题也顺带提一下,我已经在此处跌倒几次没长记性了:

首先现代浏览器,在a.com引入b.com的资源(link、img、script等),是可以正常写入与携带cookie的(ps:老版本的IE需要配置P3P),现在的广告追踪就是利用这个功能。

a.com下面通过ajax请求b.com,由于某些不可描述的原因,b.com会通过http的方式向浏览器写入cookie,未做任何处理的情况下,这种方式并不会正常写入cookie,而需要通过浏览器与服务端双方友好协商,你情我愿的情况下方可成功,流程就是:浏览器端发请求的时候告诉服务端,请同意我跨域带cookie给你,我也同意你跨域向我写cookie,如果这时候服务端应答:我愿意。

那么他们就愉快的牵手永远幸福的生活下去了。。

偏了偏了,差点写成言情小说了。

浏览器通过fetch请求时配置{credentials:'include'},通过XMLHttpRequest请求时配置xhr.withCredentialstrue,然后服务端Response Header里面添加 Access-Control-Allow-Credentials: true就OK了。 当然这也还要注意,在配置CORS相关的Response Header时,若Access-Control-Allow-Credentials的值为trueAccess-Control-Allow-Origin则不能设置为*,一来不安全,二来浏览器会报错。

参考链接: