wang1dot0 / personal-note

记录工作中遇到的个人觉得比较有意思的小问题
https://github.com/wang1dot0/personal-note
0 stars 0 forks source link

【你不知道的js(中)】 #21

Open wang1dot0 opened 4 years ago

wang1dot0 commented 4 years ago

类型与语法

内置类型

eg2. typeof function a() {} === 'function'

js中的变量是没有类型的,只有值才有。对变量执行typeof操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类型。

undefined与undeclared

js中已经声明但未赋值的变量为undefined。相反,在作用域中还没有声明的变量是undeclared。 但是,typeof并不能区分变量是undefined还是undeclared,统一返回undefined。

这是typeof的一个特殊的安全防范机制(防止报错)

数组

创建稀疏数组中的“空白单元”会出现一些异常,与赋值为undefined并不相同。 数组可以包含字符串键值和属性,但不计算长度在内。当其键值能够被强制转换为十进制数字的话,他会被当做数字索引。

delete运算符可以将单元从数组中删除,但是单元删除后,数组的length不会变化

类数组

转化为数组的方法:

Array.prototype.slice.call(arrLike);
Array.from(arrLike);

字符串

字符串的不可变性是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。 如:

var a = 'foo';
a[1] = '1';
console.log(a); // foo

字符串可以借用数组的非变更方法来处理,如join,map等 Array.prototype.join.call(str, '-') 对于可变更成员的方法,如reverse是不能像上面一样,因为字符串不可变的: c.split('').reverse().join('');

数字

js的数字类型是基于IEEE754标准实现的,主要使用双精度格式:64位二进制。 toExponential / toFixed / toPrecision 返回字符串

较小的数字

0.1 + 0.2 !== 0.3 二进制浮点数中的0.1与0.2并不是精确的 常用解决办法: 设置一个误差值,对于js来说是2^52,es6标准化为Number.EPSILON

整数的安全范围

最大整数是Number.MAX_SAFE_INTEGER=2^53 - 1 最大整数是Number.MIN_SAFE_INTEGER=-2^53 + 1

整数检测

Number.isInteger = function(num) {
  return typeof num === 'number' && num % 1 == 0;
}
Number.isSafeInteger = function(num) {
  return Number.isInteger(num) && Math.abs(num) <= Number.MAX_SAFE_INTEGER
}

特殊数值

不是值的值

undefined与null的区别: undefined: 是指没有值,从未赋值。是一个标识符,可以被当做变量使用和赋值。(非严格模式下可以被赋值,但是永远不要这么做) null:是指空值,曾赋值过,但目前没有值。是一个特殊关键字,不是标识符,不能当做变量使用和赋值。

void

表达式没有返回值,void并不影响表达式的计算,只是让void返回结果为undefined。

特殊的数字

计算结果一旦溢出为无穷数则无法回到有穷数 Infinity / Infinity 结果是NaN

值与引用

简单值总是通过值复制的方式来赋值和传递的 复合值总是通过引用复制的方式来赋值和传递的

wang1dot0 commented 4 years ago

原生函数

常用原生函数(内建函数)

内部属性 [[class]]

这个属性无法直接访问,可以通过Object.prototype.toString访问

Object.prototype.toString.call(null);
Object.prototype.toString.call(undefined);
Object.prototype.toString.call(1);
Object.prototype.toString.call('string');
Object.prototype.toString.call(true);
Object.prototype.toString.call([1,2,3]);
Object.prototype.toString.call(/a/i);
Object.prototype.toString.call(function(){});

封装对象

js基本数据类型并没有toString, length这些方法或属性,只能通过自动封装访问。 直接使用基本类型效率更高,而使用封装基本类型为对象去调用这些方法效率反而更低,这是因为浏览器做了性能优化。

有些地方值得注意:

var a = new Boolean(false);
if (!a) { /* never exec code */ }

也可以用Object(/ primitiveValue /)来手动封装,但是并不建议

拆封:一般调用对象的valueOf函数。

原生函数作为构造函数

Array

创建数组中空单元的方法

  1. 设置length大于目前的长度
  2. delete arr[i]来置空对应的元素 空单元会导致数组中某些方法的执行诡异:map,但join没有此问题,它是按照length来处理的。 如何创建多个undefined值的数组: Array.apply(null, { length: 3 }) call 方法不行,因为apply第二个参数必须为数组或类数组,内部会有一个for循环,从0到length - 1,遍历数组或类数组arr对象中的arr[0], arr[1], arr[2]等,但是这些属性都不存在,所以,值为undefined。 当然,也可以用ES6: Array.from({length: 3})

Object | Function | RegExp

尽量不要使用这三种构造函数创造对象。

Date | Error

这两个构造函数用处较多,因为没有对应的常量形式

Date new不可省略

获取当前时间戳的方法可以用getTime()获得,ES5 新增了比较简单的方法Date.now()

如果调用Date()时不带new关键字,会得到当前日期的字符串值。 1574303707

Error new可以省略

创建错误对象主要为了获得当前运行栈的上下文(一般js引擎通过只读属性.stack访问)。栈上下文信息包括函数调用栈信息和产生错误代码的行号,以便于调试。 错误对象至少包含message属性,也不乏其他只读属性。

Symbol

不能使用new

原生原型

原生构造函数都有自己的prototype。这些对象包含了其对应子类型所特有的特征。

Function.prototype是一个函数 RegExp.prototype 是一个空正则表达式 Array.prototype 是一个数组

不要轻易扩展原生原型!!!

wang1dot0 commented 4 years ago

强制类型转换

类型转换(type casting):将值从一种类型转换为另一种类型,这是显式的情况。一般发生在静态类型语言的编译阶段 强制类型转换(coercion): 隐式的情况。发生在动态类型语言的运行时 js中的强制类型转换总是返回标量基本类型值。

抽象值操作

1. ToString

基本类型值的字符串化:

原始值 结果
null 'null'
undefined 'undefined'
true 'true'
1.07e21 '1.07e21'

普通对象的toString()返回内部属性[[Class]]的值,如'[object Object]'. 但是,对象如果有自己的toStirng()方法,会覆盖原型链的方法。

JSON.stringify

对于简单值而言,与ToString效果基本相同 所有的JSON-safe都可以用这个方法序列化。并且能够呈现有效JSON格式

而遇到对象中有undefined, function, symbol时,会自动忽略 在数组中,则会转化为null(占用单元位置) 对循环引用的对象执行JSON.stringify会报错

对象中如果定义了toJSON()方法,JSON字符串化时会首先调用这个方法,然后将它的返回值进行序列化。这样我们就可以定义一个安全的JSON值。

JSON.stringify(obj, replacer, spacer); 可选参数 replacer为数组,表示要处理的属性集合,为函数,每次传入两个参数,键和值,而且是递归处理。 spacer为整数,表示缩进,为字符串,表示前面的填充。

  1. 字符串、数字、布尔值和null的JSON.stringify与ToString基本相同。
  2. 如果传给JSON.stringify的对象中定义了ToJSON方法,那么在字符串化前调用此方法,以便将对象转化为安全的json值。

2. ToNumber

原始值 结果
null 0
undefined NaN
true 1
false 0

3. ToBoolean

强制转换为布尔的假值

假值对象

document.all 类数组对象,包含页面所有dom元素。目前它是一个假值对象,但是在IE10及以下的版本,其值为真,故可以判断IE

显示强制类型转换

奇特的~运算符 反码 首先将值强制类型转换为32位数字,然后执行字位操作“非” ~x大约等同于-(x+1),则-1的反码为0 我们可以用反码来判断indexOf的结果:~(str.indexOf('c')) === true ~感觉比>=0 或者 === -1 更简洁

字位截除~~来截除数值的小数位,类似Math.floor,第一个执行ToInt32并反转字符,第二个再进行一次字位反转,即讲所有的字位反转回原值,最后得到的仍然是ToInt32的值。 但是,对负数的处理不同于Math.floor ~~x 能将值截除为一个32位整数,x | 0 也可以,但是,~~运算符优先级更高

显示解析数字字符串

var a = '42', b = '42px';
Number(a) === 42;
parseInt(a) === 42;
Number(b); // NaN
parseInt(b) === 42

解析允许字符串含有非数字字符,从左往右的顺序,如果遇到非数字字符就停止。而转换不许出现非数字字符,否则失败返回NaN。 解析与转换虽然类似,但是他们不是相互替代的关系。 parseInt针对字符串值,如果参数是其他类型的值,则首先会被强制类型转换为字符串。 第二个参数在ES5之后默认为10,之前根据字符串的第一个字符判断:x/X为十六进制

parseInt(1/0, 19) === 18; ???
parseInt(0.000008) === 0;
parseInt(0.0000008) === 8;
parseInt(false, 16) === 16;
parseInt(parseInt, 16) === 16;
parseInt('0x10') === 16;
parseInt('103', 2) === 2

显示转换为布尔值

建议使用:Boolean(...) 或 !!(...)

隐式强制类型转换

作用: 减少冗余,让代码看起来简洁

1 字符串与数字之间的隐式强制类型转换

2 布尔值到数字的隐式强制类型转换

3 隐式强制类型转换为布尔值

发生布尔值隐式强制类型转换的地方:

4 && 与 ||

返回值不一定是布尔类型,而是两个操作数其中一个的值 a || b 与 a ? a : b 的区别 而用在if()表达式中,它会发生隐式强制类型转换为boolean。

5 符号的强制类型转换

字符串: 隐式转换发生错误,显示可以转换 布尔值:显示/隐式转换都是true 数字: 显示/隐式转换都会发生错误

宽松相等与严格相等

==允许在相等比较中进行强制类型转换, ===不允许

抽象相等

ES5 规范 http://www.ecma-international.org/ecma-262/5.1/index.html#sec-11.9.3

  1. 两个值如果类型相同,则仅比较值相等。

    NaN 不等于 NaN, -0 等于 0

  2. 如果是两个对象比较,则指向同一个值时视为相等,不发生强制类型转换。

  3. 如果两个值类型不相同,会将其中一个或者两个的值转化为相同类型

    • 字符串与数字比较 会把字符串进行ToNumber操作,而后比较

    • 其他类型与布尔值之间的比较

    • 如果Type(x)是布尔类型,则返回 ToNumber(x) == y 的结果

    • 如果Type(y)是布尔类型,则返回 x == ToNumber(y) 的结果 var a = '42'; a == false; a == true; 两个表达式都返回false,说明a = ‘42’是个假值么? 并不是,这里并没有进行ToBoolean的比较,也就是说这里‘42’并没有转化布尔值,而是将布尔值转化为了数字,‘42’转化成了42,进行的比较。所以,讨论 a='42' 是真值还是假值没有意义。 所以,写if(a == true)这种用法是错误的,可以选择 if(a) 或 if(!!a) 或 if(Boolean(a))

    • null 与undefined之间的相等比较

    • 如果x为null, y 为undefined,则结果为true

    • 如果x为undefined, y为null,则结果为true 在==下,null与undefined相等,除此之外,其他值与他们都不相等。因此,可以将null和undefined作为等价值处理。如 if (null == undefined)

    • 对象与非对象之间的相等比较

    • 如果Type(x)是字符串或数字,Type(y)是对象,则返回 x == ToPrimitive(y) 的结果

    • 如果Type(x)是对象,Type(y)是字符串或数字,则返回 ToPrimitive(x) == y 的结果

几种特殊的情况:null, undefined, NaN

var a = null, b = undefined, c = NaN;
var ao = Object(a),
      bo = Object(b),
      co = Object(c);

a == ao;
b == bo;
c == co;         // false

因为null和undefined没有封装对象,Object(null) 与 Object()都返回一个常规对象 而NaN可以封装为一个对象,但是拆封之后NaN与自己不相等 所以,以上三个都是false。

比较少见的情况

返回其他数字 重新定义内置原生原型的valueOf 假值的相等比较

'0' == null;
'0' == undefined;
'0'  == false;
'0' == NaN;
'0' == 0;
'0' == '';

false == null; 
false = undefined;
false == NaN;
false == 0;
false == '';
false == [];
false == {};

'' == null;
'' == undefined;
'' == NaN;
'' == 0;
'' == [];
'' == {};

0 == null;
0 == undefined;
0 == NaN;
0 == [];
0 == {};

极端情况

  1. [] == ![]; // true 因为右边被强制转换为Boolean;
  2. '' == [null]; [null] 会被直接转换为'';
  3. 0 == '\n';0 == ' '; 均为true。

完整性检查 7种很坑的地方:

‘0’ == false;
false == 0;
false == '';
false == [];
'' == 0;
'' == [];
0 == [];

在==中避免使用false,所以,只剩下后三种。要小心。

安全运用隐式强制类型转换

这时最好用===来避免不经意间的强制类型转换

抽象关系比较

var a = { name: 42 }
var b = { name: 43 }
a > b
a == b
a < b
which one is true?
a <= b ? 他会转化为!(a > b) 所以为true

双方都调用ToPrimitive,如果结果出现非字符串,就根据ToNumber规则将双方强制转换为数字比较 关系比较并没有严格关系比较。如何确保在关系比较中不发生类型转换呢?只能保持两个数的类型不变。

wang1dot0 commented 4 years ago

语法

try...finally

finally的代码总在try之后执行,如果有catch的话则在catch之后执行。

function foo() {
  try {
    return 42;
  }
  finally {
    console.log('Hello');
  }
  console.log('never runs');
}
console.log( foo() );
// Hello
// 42

先执行return 42,并将foo()函数的返回值设为42。然后接着执行finally。 try中的throw也是如此:

function foo() {
  try {
    throw 42;
  }
  finally {
    console.log('Hello');
  }
  console.log('never runs');
}
console.log( foo() );
// Hello
// Uncaught Exception: 42

如果finally中抛出异常,函数就会在此处终止。如果此前try中已经有return设置了返回值,则该值被丢弃:

function foo() {
  try {
    return 42;
  }
  finally {
    throw 'Oops!';
  }
  console.log('never runs');
}
console.log( foo() );
// Uncaught Exception: Oops!

finally中的return会覆盖try和catch中的return返回值

function foo() {
  try {
    return 42;
  }
  finally {
    // 无return
  }
}
function bar() {
  try {
    return 42;
  }
  finally {
    return;
  }
}
function baz() {
  try {
    return 42;
  }
  finally {
    return 'Hello';
  }
}
foo(); // 42
bar(); // undefined
baz(); // 'Hello'

保留字

实现中的限制

wang1dot0 commented 4 years ago

异步与性能

异步:现在与将来

异步控制台 console.*方法族是由宿主环境提供的。某些浏览器的console.log不会把传入的内容立刻输出。 原因是,在许多程序中,I/O是非常低速的阻塞部分。 所以,浏览器在后台异步处理控制台I/O能够提高性能,但是用户甚至根本感受不到其发生。 预防方法:

  1. 在JavaScript调试器中使用断点,而不要依赖控制台输出。
  2. 把需要打印的对象提前序列化,做一次快照,如JSON.stringify(...)

1.2 事件循环

JavaScript植入的各个环境都有一个共同"点",即他们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用Javascript引擎,这种机制被称为事件循环。 换句话说,JavaScript引擎本身并没有时间概念,只是一个按需执行JavaScript任意代码片段的环境。“事件”调度总是由包含它的宿主环境进行。

1.3 并行线程

JavaScript不考虑线程共享数据的问题。

完整运行

一个函数foo的代码具有原子性,也就是foo中代码一旦开始运行,就会执行完函数内部所有代码之后,才开始执行其他函数。这称为完整运行(run-to-completion)。 函数顺序的不确定性就是竞态条件

1.4 并发

单线程事件循环是并发的一种形式。

非交互

如果进程间没有相互影响的话,不确定性是完全可以接受的。

交互

需要进行特定的处理来保证顺序,达到目的。

协作

这里的目标是取到一个长期运行的“进程”,并将其分割成多个步骤或多批任务,使得其他开发“进程”有机会将自己的运算插入到事件循环队列中交替运行。

var res = [];
function response(data) {
  res = res.concat(
    data.map((val) => val * 2);
  );
}
ajax('url/1', response);
ajax('url/2', response);

如果有1000万条数据,会运行很长一段时间,使浏览器出现假死。 这里有一个简单的方法:依靠异步批处理这些结果。

var res = [];
function response(data) {
  var chunk = data.splice(0, 1000);

  res = res.concat(
    chunk.map((val) => val * 2);
  );
  if (data.length > 0) {
    setTimeout(() => {
      response(data);
    }, 0);
  }
}

ajax('url/1', response);
ajax('url/2', response);

1.5 任务

ES6新增概念: 任务队列。 任务队列是挂在事件循环队列的每个tick之后的一个队列。在事件循环的每个tick中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会再当前tick的任务队列末尾添加一个任务。

1.6 语句顺序

代码中语句的顺序和js引擎执行语句的顺序并不一定一致。

wang1dot0 commented 4 years ago

Promise

鸭子类型:看起来像鸭子,叫起来像鸭子,那它就是鸭子

Promise的信任问题

回调的问题:

调用过早

Zalgo副作用:任务有时候同步完成,有时候异步完成。 new Promise(r => r(42)) 即使立即完成,then的回调也会被异步调用

调用过晚

const p = new Promise(r => r());
p.then(() => {
  p.then(() => {
    console.log('C');
  });
  console.log('A');
});
p.then(() => {
  console.log('B');
});

输出顺序是:A B C。内部的C不会阻止或者抢占B的位置。

回调未调用

  1. 没有任何东西能阻止Promise向你通知它的决议。
  2. 如果Promise本身永远不被决议:竞态机制
    
    function timeoutPromise(delay) {
    return new Promise((resolve, reject) => {
    setTimeout(function() {
      reject( 'Timeout!' );
    }, delay);
    });
    }

Promise .race([ foo, timeoutPromise(3000) ]) .then(function() { // foo及时完成 }, function() { // 超时了 });



#### 调用次数过多或过少
Promise只会决议一次。通过then的调用也只有一次。

#### 未能传递参数/环境值
使用多个参数调用resolve/reject,第一个后面的参数会被忽略。

#### 吞掉错误或异常

### 错误处理
`try...catch` 只适用于同步开发

#### 绝望的陷阱
在promise链的末尾加catch,听起来不错,但是如果catch中的回调出问题了,谁来捕获呢?
#### 处理未捕获的情况
#### 成功的坑

### Promise模式
#### Promise.all
wang1dot0 commented 4 years ago

Promise API 概述

1 new Promise(...)构造器

内部的回调是同步的或立即调用的。这个函数接受两个函数回调,用来支持promise的决议:resolve与reject。 reject表示拒绝这个promise,而resolve既可能完成promise,也可能拒绝,根据参数决议。如果传入的参数是一个非promise,非thenable的立即值,这个promise就会用这个值完成。但是,如果传入的是一个真正的promise或thenable的值,这个值会被递归展开,并且promise会取用最终决议值。

2 Promise.resolve(...)和Promise.reject(...)

类似第一条中回调函数中的两个参数。但是,如果传入Promise.reoslve(...)的是一个真正的Promise,他会直接返回这个值。

3. then(...)和catch(...)

4. Promise.all([...])和Promise.race([...])

如果传入空数组,all会立刻完成,而race会pending,永远不会决议。

Promise局限性

1. 顺序错误处理

2. 单一值

3. 单决议

4. 惯性

wang1dot0 commented 4 years ago

Generator

wang1dot0 commented 4 years ago

程序性能

Web Worker

Worker之间以及和主程序之间,不会共享任何作用域和资源 Worker内部是无法访问主程序的任何资源的。意味着不能访问它的全局变量,不能访问页面的DOM或者其他资源。 但是,可以执行网络操作(Ajax, WebSockets)以及设定定时器。worker可以访问几个重要的全局变量和功能的本地副本: navigator location JSON 和applicationCache 可以使用importScripts(...)加载额外的js脚本,这些加载是同步的。

importScripts('foo.js', 'bar.js');

数据传递

当worker与主线程之间需要传递JSON数据时:

  1. 早期用序列化/反序列化对象: 缺点是序列化的操作会导致速度慢,同时数据的复制导致两倍的内存使用。
  2. 结构化克隆算法 缺点依旧是数据的复制导致两倍的内存使用。IE10+和主流浏览器支持
  3. Transferable对象: Uint8Array 对象所有权的转移,对象本身没有移动
    // 比如foo是一个Uint8Array,第一个参数是原始缓存区,第二个参数是要传输的内容的列表
    postMessage(foo.buffer, [foo.buffer]);

    共享 Worker

    站点或者APP加载同一个页面的多个tab,希望防止重复专用Worker来降低系统的资源调用。类似有限资源的socket网络连接,因为浏览器限制了到同一个主机的同时连接数目。 创建一整个站点或APP的所有页面实例都可以共享的中心Worker,这叫做SharedWorker

    // 只有Firefox与Chrome支持
    var w = new SharedWorker('xx.js');
    // 初始化
    w.port.start();
    // 通过port来识别消息来源。
    w.port.addEventListener('message', handleMessages);
    w.port.postMessage('Something cool.');

    在共享的worker内部,要处理一个connect事件。这个事件为特定的链接提供了端口对象。

    addEventListener('connect', function(evt) {
    var port = evt.ports[0];
    port.addEventListener('message', function(evt) {
    port.postMessage(...);
    });
    port.start();
    })

模拟Web Worker

浏览器不支持Worker时,从性能的角度来说是没法模拟多线程的。

wang1dot0 commented 4 years ago

性能测试与调优

性能测试

重复

Benchmark.js

如果你想要对你的代码进行功能测试与性能测试,这个库最优先考虑。 setup/teardown 可以定义每次测试之前和之后调用的函数。不是在每个测试迭代都运行。而是在每次外层循环的开始和结束处进行,而不是在内层循环中。

环境为王