这些javascript细节,你未必知道(代码片段)

前端开发博客 前端开发博客     2023-02-02     467

关键词:

大家好,我是 漫步,今天分享一篇关于JS 细节的文章,喜欢记得点关注我并设为星标哦

前言

本文主要给大家带来一些我读《你不知道的 JavaScript(中卷)》中遇到的一些有意思的内容,可以说是打开新世界的大门的感觉。希望能在工作之余,给大家带来一点乐趣。

JavaScript 是一门优秀的语言。只学其中一部分内容很容易,但是要全面掌握则很难。开发人员遇到困难时往往将其归咎于语言本身,而不反省他们自己对语言的理解有多匮乏。《你不知道的 JavaScript》旨在解决这个问题,使读者能够发自内心地喜欢上这门语言。

强制类型转换

值类型转换

var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String(a); // 显式强制类型转换

抽象值操作

document.all 是假值对象。也就是 !!document.all 值为 false

显示强制类型转换

日期显示转换为数字:

使用 Date.now() 来获得当前的时间戳,使用 new Date(..).getTime() 来获得指定时间的时间戳。

奇特的 ~ 运算符:

~x 大致等同于 -(x+1)。很奇怪,但相对更容易说明问题:~42; // -(42+1) ==> -43

JavaScript 中字符串的 indexOf(..) 方法也遵循这一惯例,该方法在字符串中搜索指定的子 字符串,如果找到就返回子字符串所在的位置(从 0 开始),否则返回 -1。

~indexOf() 一起可以将结果强制类型转换(实际上仅仅是转换)为真 / 假值:

var a = "Hello World";
~a.indexOf("lo"); // -4 <-- 真值!

if (~a.indexOf("lo"))  // true
  // 找到匹配!

解析非字符串:

曾经有人发帖吐槽过 parseInt(..) 的一个坑:

parseInt( 1/0, 19 ); // 18

parseInt(1/0, 19) 实际上是 parseInt("Infinity", 19)。第一个字符是 "I",以 19 为基数 时值为 18。

此外还有一些看起来奇怪但实际上解释得通的例子:

parseInt(0.000008); // 0 ("0" 来自于 "0.000008")
parseInt(0.0000008); // 8 ("8" 来自于 "8e-7")
parseInt(false, 16); // 250 ("fa" 来自于 "false")
parseInt(parseInt, 16); // 15 ("f" 来自于 "function..")
parseInt("0x10"); // 16
parseInt("103", 2); // 2

隐式强制类型转换

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

例如:

var a = "42";
var b = "0";
var c = 42;
var d = 0;
a + b; // "420"
c + d; // 42

再例如:

var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"

根据 ES5 规范 11.6.1 节,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作(规范 9.1 节),该抽象操作再调用 [[DefaultValue]](规范 8.12.8 节),以数字作为上下文。

你或许注意到这与 ToNumber 抽象操作处理对象的方式一样(参见 4.2.2 节)。因为数组的 valueOf() 操作无法得到简单基本类型值,于是它转而调用 toString()。因此上例中的两个数组变成了 "1,2" 和 "3,4" 。+ 将它们拼接后返回 "1,23,4" 。

简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤可以得到字符串),则执行字符串拼接;否则执行数字加法。

符号的强制类型转换

ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误,具体的原因不在本书讨论范围之内。

例如:

var s1 = Symbol("cool");
String(s1); // "Symbol(cool)"
var s2 = Symbol("not cool");
s2 + ""; // TypeError

符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true)。

由于规则缺乏一致性,我们要对 ES6 中符号的强制类型转换多加小心。

好在鉴于符号的特殊用途,我们不会经常用到它的强制类型转换。

宽松相等和严格相等

常见的误区是“== 检查值是否相等,=== 检查值和类型是否相等”。听起来蛮有道理,然而还不够准确。很多 JavaScript 的书籍和博客也是这样来解释的,但是很遗憾他们都错了。

正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”

字符串和数字之间的相等比较:

  • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。

  • 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

其他类型和布尔类型之间的相等比较:

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

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

nullundefined 之间的相等比较:

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

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

对象和非对象之间的相等比较:

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

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

语法

错误

提前使用变量

ES6 规范定义了一个新概念,叫作 TDZ(Temporal Dead Zone,暂时性死区)。

TDZ 指的是由于代码中的变量还没有初始化而不能被引用的情况。

对此,最直观的例子是 ES6 规范中的 let 块作用域:


  a = 2; // ReferenceError!
  let a;

a = 2 试图在 let a 初始化 a 之前使用该变量(其作用域在 .. 内),这里就是 a 的 TDZ,会产生错误。

有意思的是,对未声明变量使用 typeof 不会产生错误(参见第 1 章),但在 TDZ 中却会报错:


  typeof a; // undefined
  typeof b; // ReferenceError! (TDZ)
  let b;

回调

省点回调

构造一个超时验证工具:

function timeoutify(fn, delay) 
  var intv = setTimeout(function() 
    intv = null
    fn(new Error('Timeout!'))
  , delay)

  return function() 
    // 还没有超时?
    if (intv) 
      clearTimeout(intv)
      fn.apply(this, arguments)
    
  

以下是使用方式:

// 使用 ‘error-first 风格’ 回调设计
function foo(err, data) 
  if (err) 
    console.error(err)
  
  else 
    console.log(data)
  


ajax('http://some.url.1', timeoutify(foo, 500))

如果你不确定关注的 API 会不会永远异步执行怎么办呢?可以创建一个类似于这个“验证概念”版本的 asyncify(..) 工具:

function asyncify(fn) 
  var orig_fn = fn,
    intv = setTimeout(function() 
      intv = null
      if (fn) fn()
    , 0)

  fn = null

  return function() 
    // 触发太快,在定时器intv触发指示异步转换发生之前?
    if (intv) 
      fn = orig_fn.bind.apply(
        orig_fn,
        // 把封装器的this添加到bind(..)调用的参数中,
        // 以及克里化(currying)所有传入参数
        [this].concat([].slice.call(arguments))
      )
    
    // 已经是异步
    else 
      // 调用原来的函数
      orig_fn.apply(this, arguments)
    
  

可以像这样使用 asyncify(..)

function result(data) 
  console.log(a)


var a = 0

ajax('..pre-cached-url..', asyncify(result))
a++

不管这个 Ajax 请求已经在缓存中并试图对回调立即调用,还是要从网络上取得,进而在将来异步完成,这段代码总是会输出 1,而不是 0——result(..) 只能异步调用,这意味着 a++ 有机会在 result(..) 之前运行。

关于回调地狱的可以看:JS中优雅的使用async await

Promise

Promise 信任问题

回调未调用

提供一个超时处理的解决方案:

// 用于超时一个Promise的工具
function timeoutPromise(delay) 
  return new Promise(function(resolve, reject) 
    setTimeout(function() 
      reject('Timeout!')
    , delay)
  )


// 设置foo()超时
Promise.race([
  foo(),
  timeoutPromise(3000)
])
.then(
  function() 
    // foo(..)及时完成!
  ,
  function(err) 
    // 或者foo()被拒绝,或者只是没能按时完成
  // 查看err来了解是哪种情况
  
)

链式流

为了进一步阐释链接,让我们把延迟 Promise 创建(没有决议消息)过程一般化到一个工具中,以便在多个步骤中复用:

function delay(time) 
  return new Promise(function(resolve, reject) 
    setTimeout(resolve, time)
  )


delay(100) // 步骤1
  .then(function STEP2() 
    console.log("step 2 (after 100ms)")
    return delay(200)
  )
  .then(function STEP3() 
    console.log("step 3 (after another 200ms)")
  )
  .then(function STEP4() 
    console.log("step 4 (next Job)")
    return delay(50)
  )
  .then(function STEP5() 
    console.log("step 5 (after another 50ms)")
  )

调用 delay(200) 创建了一个将在 200ms 后完成的 promise,然后我们从第一个 then(..) 完成回调中返回这个 promise,这会导致第二个 then(..) 的 promise 等待这个 200ms 的 promise。

Promise 局限性

顺序错误处理

Promise 的设计局限性(链式调用)造成了一个让人很容易中招的陷阱,即 Promise 链中的错误很容易被无意中默默忽略掉。

关于 Promise 错误,还有其他需要考虑的地方。由于一个 Promise 链仅仅是连接到一起的成员 Promise,没有把整个链标识为一个个体的实体,这意味着没有外部方法可以用于观察可能发生的错误。

如果构建了一个没有错误处理函数的 Promise 链,链中任何地方的任何错误都会在链中一直传播下去,直到在某个步骤注册拒绝处理函数。在这个特定的例子中,只要有一个指向链中最后一个 promise 的引用就足够了(下面代码中的 p),因为你可以在那里注册拒绝处理函数,而且这个处理函数能够得到所有传播过来的错误的通知:

// foo(..), STEP2(..)以及STEP3(..)都是支持promise的工具
var p = foo(42)
  .then(STEP2)
  .then(STEP3);

虽然这里可能令人迷惑,但是这里的 p 并不指向链中的第一个 promise(调用 foo(42) 产生的那一个),而是指向最后一个 promise,即来自调用 then(STEP3) 的那一个。

还有,这个 Promise 链中的任何一个步骤都没有显式地处理自身错误。这意味着你可以在 p 上注册一个拒绝错误处理函数,对于链中任何位置出现的任何错误,这个处理函数都会得到通知:

p.catch(handleErrors);

但是,如果链中的任何一个步骤事实上进行了自身的错误处理(可能以隐藏或抽象的不可见的方式),那你的 handleErrors(..) 就不会得到通知。这可能是你想要的——毕竟这是一个“已处理的拒绝”——但也可能并不是。不能清晰得到(对具体某一个“已经处理”的拒绝的)错误通知也是一个缺陷,它限制了某些用例的功能。

基本上,这等同于 try..catch 存在的局限:try..catch 可能捕获一个异常并简单地吞掉它。所以这并不是 Promise 独有的局限性,但可能是我们希望绕过的陷阱。

遗憾的是,很多时候并没有为 Promise 链序列的中间步骤保留的引用。因此,没有这样的引用,你就无法关联错误处理函数来可靠地检查错误。

关于Promise你还可以看这个:一道让人失眠的 Promise 试题深入分析

单一值

根据定义,Promise 只能有一个完成值或一个拒绝理由。在简单的例子中,这不是什么问题,但是在更复杂的场景中,你可能就会发现这是一种局限了。

一般的建议是构造一个值封装(比如一个对象或数组)来保持这样的多个信息。这个解决方案可以起作用,但要在 Promise 链中的每一步都进行封装和解封,就十分丑陋和笨重了。

  1. 分裂值

有时候,你可以把这一点,当作提示你应该把问题分解为两个或更多 Promise 的信号。

设想你有一个工具 foo(..),它可以异步产生两个值(x 和 y):

function getY(x)  
  return new Promise(function(resolve, reject) 
    setTimeout(function() 
      resolve((3 * x) - 1); 
    , 100); 
  );
 

function foo(bar, baz)  
  var x = bar * baz; 
  return getY(x).then(function(y) 
    // 把两个值封装到容器中
    return [x, y]; 
  ); 
 

foo(10, 20).then(function(msgs) 
  var x = msgs[0]; 
  var y = msgs[1]; 
  console.log(x, y); // 200 599 
);

首先,我们重新组织一下 foo(..) 返回的内容,这样就不再需要把 xy 封装到一个数组值中以通过 promise 传输。取而代之的是,我们可以把每个值封装到它自己的 promise:

function foo(bar, baz)  
  var x = bar * baz; 
  
  // 返回两个 promise
  return [ 
    Promise.resolve(x), 
    getY(x) 
  ]; 
 

Promise.all( 
  foo(10, 20) 
).then(function(msgs) 
  var x = msgs[0]; 
  var y = msgs[1]; 
  console.log(x, y); 
);

一个 promise 数组真的要优于传递给单个 promise 的一个值数组吗?从语法的角度来说,这算不上是一个改进。

但是,这种方法更符合 Promise 的设计理念。如果以后需要重构代码把对 xy 的计算分开,这种方法就简单得多。由调用代码来决定如何安排这两个 promise,而不是把这种细节放在 foo(..) 内部抽象,这样更整洁也更灵活。这里使用了 Promise.all([ .. ]),当然,这并不是唯一的选择。

  1. 传递参数

var x = ..var y = .. 赋值操作仍然是麻烦的开销。我们可以在辅助工具中采用某种函数技巧:

function spread(fn)  
  return Function.apply.bind(fn, null); 
 

Promise.all( 
  foo(10, 20) 
).then(spread(function(x, y) 
  console.log(x, y); // 200 599 
))

这样会好一点!当然,你可以把这个函数戏法在线化,以避免额外的辅助工具:

Promise.all( 
  foo(10, 20) 
).then(Function.apply.bind( 
  function(x, y) 
    console.log(x, y); // 200 599 
  ,
  null
));

这些技巧可能很灵巧,但 ES6 给出了一个更好的答案:解构。数组解构赋值形式看起来是这样的:

Promise.all( 
  foo(10, 20) 
).then(function(msgs) 
  var [x, y] = msgs; 
  console.log(x, y); // 200 599 
);

不过最好的是,ES6 提供了数组参数解构形式:

Promise.all( 
  foo(10, 20) 
) 
.then(function([x, y]) 
  console.log(x, y); // 200 599 
);

现在,我们符合了“每个 Promise 一个值”的理念,并且又将重复样板代码量保持在了最小!

单决议

Promise 最本质的一个特征是:Promise 只能被决议一次(完成或拒绝)。在许多异步情况中,你只会获取一个值一次,所以这可以工作良好。

但是,还有很多异步的情况适合另一种模式——一种类似于事件或数据流的模式。在表面上,目前还不清楚 Promise 能不能很好用于这样的用例,如果不是完全不可用的话。如果不在 Promise 之上构建显著的抽象,Promise 肯定完全无法支持多值决议处理。

设想这样一个场景:你可能要启动一系列异步步骤以响应某种可能多次发生的激励(就像是事件),比如按钮点击。

这样可能不会按照你的期望工作:

// click(..) 把"click"事件绑定到一个 DOM 元素
// request(..) 是前面定义的支持 Promise 的 Ajax 
var p = new Promise(function(resolve, reject) 
  click("#mybtn", resolve); 
); 

p.then(function(evt) 
  var btnID = evt.currentTarget.id; 
  return request("http://some.url.1/?id=" + btnID); 
).then(function(text) 
  console.log(text); 
);

只有在你的应用只需要响应按钮点击一次的情况下,这种方式才能工作。如果这个按钮被点击了第二次的话,promise p 已经决议,因此第二个 resolve(..) 调用就会被忽略。

因此,你可能需要转化这个范例,为每个事件的发生创建一整个新的 Promise 链:

click("#mybtn", function(evt) 
  var btnID = evt.currentTarget.id; 
  request("http://some.url.1/?id=" + btnID).then(function(text) 
    console.log(text); 
  ); 
);

这种方法可以工作,因为针对这个按钮上的每个 "click" 事件都会启动一整个新的 Promise 序列。

由于需要在事件处理函数中定义整个 Promise 链,这很丑陋。除此之外,这个设计在某种程度上破坏了关注点与功能分离(SoC)的思想。你很可能想要把事件处理函数的定义和对事件的响应(那个 Promise 链)的定义放在代码中的不同位置。如果没有辅助机制的话,在这种模式下很难这样实现。

感谢

如果本文对你有帮助,就点个赞支持下吧!感谢阅读。

关于本文

作者:gyx_这个杀手不太冷静
https://juejin.cn/post/6859133591108976648

- EOF -

推荐阅读  点击标题可跳转

不为人知的 JavaScript 技巧

20 个杀手级 JavaScript 单行代码

16个必备的JavaScript代码片段

觉得本文对你有帮助?请分享给更多人

推荐关注「前端开发博客」,提升前端技能

我是漫步,分享技术,不止前端,下期见~

文中如有错误,欢迎给我留言,如果这篇文章帮助到了你,欢迎点赞、在看和关注。你的点赞、在看和关注是对我最大的支持!

创作不易,你的每一个点赞、在看、分享都是对我最大的支持!❤️

这些比较规则,你未必都知道

  相等运算符:  使用相等运算符来比较两个值是否相等,相等返回true,否则返回false。  1、对于简单类型来说,如数字、布尔值、字符串,比较的是两者的值是否相等。  1==1  //true  2==1  //false  true==true ... 查看详情

你需要知道的javascript类(class)的这些知识(代码片段)

...,大家面试可以参照考点复习,希望我们一起有点东西。JavaScript使用原型继承:每个对象都从原型对象继承属性和方法。在Java或 查看详情

这些mos管的特点和分类,你未必全都知道

   在市场上,MOS管存在的类型多的让你不知道如何选择,因为它毕竟是一种产品,会不断的被更新并且研发出来。  而就在今天,立深鑫MOS管代理商为您分享一个知识点:让你清楚什么类型的MOS管有着怎么样的特点,以至... 查看详情

关于hashmap你需要知道的一些细节(代码片段)

本文的公众号文章链接:关于HashMap你需要知道的一些细节在官方文档中的描述:HashtablebasedimplementationoftheMapinterface.Thisimplementationprovidesalloftheoptionalmapoperations,andpermitsnullvaluesandthenullkey.(TheHashMa 查看详情

你可能不知道的14个javascript调试技巧(代码片段)

以更快的速度和更高的效率调试你的JavaScript了解你的工具可以在完成任务的过程中发挥重大作用。尽管传言JavaScript难以调试,但是如果你掌握了一些调试技巧,那么你将会花费更少的时间来解决这些错误。我们已经列出了14个... 查看详情

kafka的这些原理你知道吗(代码片段)

如果只是为了开发Kafka应用程序,或者只是在生产环境使用Kafka,那么了解Kafka的内部工作原理不是必须的。不过,了解Kafka的内部工作原理有助于理解Kafka的行为,也利用快速诊断问题。下面我们来探讨一下这三个问题Kafka是如何... 查看详情

这些强大的js操作符,你知道几个?(代码片段)

本文约 8600 字,预计阅读需要30 分钟。JavaScript为我们提供了很多操作符,用于操作表达式。下面就来盘点一下JavaScript中那些强大的操作符!一、一元操作符操作符可以根据他们期待的操作符个数来分类,多数... 查看详情

跨域你需要知道这些(代码片段)

...策略是浏览器最核心最基础的安全策略现在所有的可支持Javascript的浏览器都会使用这个策略web构建在同源策略 查看详情

javascript中的这些骚操作,80%的人都不知道(代码片段)

引言??1.写这篇文章的缘由是上周在公司前端团队的codereview时,看了一个实习小哥哥的代码后,感觉一些刚入行不久的同学,对于真实项目中的一些js处理不是很熟练,缺乏一些技巧。2.因此整理了自己开发中常用的一些js技巧,... 查看详情

8个你可能不知道答案的常见javascript面试问题(代码片段)

不管你喜不喜欢,棘手的问题仍然会被野外的面试官问到。 原因是,这些问题可以告诉你很多关于你对语言的核心理解,因此你是否适合这份工作。这些问题中涉及的常见概念包括:Hoisting关闭范围值与引用类型原型继承今... 查看详情

你应该知道的es2020中的10个javascript新特性(代码片段)

...们现在对ES2020中发生的变化有了完整的了解,ES2020是JavaScript的新的和改进的规范。因此,让我们看看这些变化是什么。1.BigIntBigInt是JavaScript中最令人期待的功能之一,终于来了。实际上,它允许开发人员在其JS代... 查看详情

你所不知道的c#中的细节(代码片段)

...东西,但是很多开发者并不知道,因此也就没法好好利用这些东西,那么今天我细数一下这些藏在编译器中的细节。不是只有Task和ValueTask才能await在C#中编写异步代码的时候,我们经常会选择将异步代码包含在一个Task或者ValueTask... 查看详情

javascript优雅的实现方式包含你可能不知道的知识点

有些东西很好用,但是你未必知道;有些东西你可能用过,但是你未必知道原理。实现一个目的有多种途径,俗话说,条条大路通罗马。很多内容来自平时的一些收集以及过往博客文章底下的精彩评论,收集整理拓展一波,发散... 查看详情

2021年要了解的34种javascript简写优化技术(代码片段)

...东西,跟上变化不应该比现在更难,我的动机是介绍所有JavaScript的最佳实践,比如简写功能,作为一个前端开发者,我们必须知道,让我们的生活在2021年变得更轻松。你可能做了很长时间的JavaScript开发,但有时你可能没有更新... 查看详情

springboot属性配置你所不知道的细节(代码片段)

今天我们要聊的这个问题,可能工作5年的资深程序员也不一定搞得很清楚,但是我敢保证在开发Web应用过程中大家都遇到过。这个问题就是:SpringBoot应用程序读取配置属性时,不同配置源的优先级是个啥情况?那... 查看详情

《你知道的javascript》——混合对象类(代码片段)

我今天的问题是关于这三个功能:publicvoidgenerateCalendarEvents(finalStringid,finalMap<String,String>params);publicvoidgenerateCalendarEvents(Objectobject,finalStringid,finalMap<String,String>params);publ 查看详情

你可能不知道的javascript代码片段和技巧(下)(代码片段)

JavaScript是一个绝冠全球的编程语言,可用于Web开发、移动应用开发(PhoneGap、Appcelerator)、服务器端开发(Node.js和Wakanda等等。JavaScript还是很多新手踏入编程世界的第一个语言。既可以用来显示浏览器中的简单提示框,也可以通... 查看详情

你可能不知道的javascript代码片段和技巧(上)(代码片段)

JavaScript是一个绝冠全球的编程语言,可用于Web开发、移动应用开发(PhoneGap、Appcelerator)、服务器端开发(Node.js和Wakanda等等。JavaScript还是很多新手踏入编程世界的第一个语言。既可以用来显示浏览器中的简单提示框,也可以通... 查看详情