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
35
/*
* @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
3
4
5
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
var obj = {};

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

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

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

1
export const hasProto = '__proto__' in {}

instanceof 原理

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

1
A instanceof B

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

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”的情况必须被列为违反规则的特殊情况。

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 同其他

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
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
  • $$: document.querySelectorAll

像jq。
如:

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

17 document.visibilityState

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

1
2
3
4
5
6
7
8
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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	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属性。

类似于

``` js
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
{ 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
/**
* @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
13
14
15
16
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
10
/**
* @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
13
14
15
16
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

使用

当年的a标签

1
2
3
4
<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();
}();

不受“管束”的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赋值操作进行报错,只是视为一条无用的表达式。

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