这些特性可真是js中的卧龙凤雏

533 阅读7分钟

JavaScript 的诞生背景

在互联网发展初期,也就是还没有 JavaScript 的时候,浏览器端的某些表单校验只能发送到服务器端进行执行。那时候的人们还在使用速度仅为 28.8kbit/s 的 “猫” 上网,其速度之慢,实在让人抓狂。想象一下,你填完一个表单点击提交后,经过漫长的 30 秒等待,服务器最终返回了某个必填字段为空,这时你会......

640.gif

当时走在技术革新最前沿的 Netscape 公司决定开发一款客户端语言来补救这种缺陷。就职于该公司的布兰登·艾奇(Brendan Eich)开始着手进行开发设计。由于工期紧张加之个人当时的兴趣爱好,该大佬仅用十来天的时间就设计出来了这门语言。

当时 Netscape 将其称为 LiveScript,但当时 Sun 公司的 Java 语言正如日中天。 Netscape 为了搭上媒体热炒 Java 的顺风车(就是蹭热度啦),在发布前夕将 LiveScript 改名为 JavaScript

这像极了咱们的开发历程:怎么实现我不管,反正今天要上线!也正是由于设计时间短,语言的一些细节考虑得不够严谨,导致后来很长一段时间,JavaScript 写出来的程序饱受吐槽。如果 Brendan Eich 预见到,他设计的 JavaScript 在将来全世界有几百万的学习者,而且会成为互联网倍受欢迎的编程语言,他会不会多花一点时间呢?

今天咱们一起来了解一下 JavaScript 中的卧龙凤雏。

从零手写简版 Vue2/Vue3、从零手写 Promise 完整源码、从零使用 typescript 手造轮子封装 axios、手把手教你搭建 node+egg 项目等等高质量资源 来这里 发送「1024」获取吧!!!

全局变量

JavaScript 对全局变量具有依赖性是一种特别糟糕的特性。有全局变量这是许多编程语言所允许的,而在 JavaScript 中,它允许在任何地方依赖全局变量。比如我使用 script 的方式引入了 a、b、c 三个文件后将可以直接在自己的文件 d 中使用其他文件中声明的全局变量。

这在自己的 d 文件中将会有一种困惑:这个全局变量是从哪里来的,如果出现问题该去哪个地方进行排查。另外,如果在 d 文件中声明了和这个全局变量相同命名的变量将会对其覆盖,这导致在之后再引入的文件中不能使用前面三个文件中定义的重名全局变量。

而定义全局变量的这几种方法:

  1. 脱离任何函数直接在顶层安排一个 var 语句:

    var foo = 'foo'
    
  2. 直接添加一个属性到全局对象上。在浏览器端,全局对象名为 window

    window.foo = 'foo'
    
  3. 直接使用未经过声明的变量,这称为隐式的全局变量:

    foo = 'foo'
    

因此在实际的开发中,咱们要务必避免使用全局变量而使用局部变量。比如使用立即调用函数加开启严格模式的形式:

(function() {
  'use strict'
  // 这里的变量将会成为局部变量
  // 开启严格模式将不能为未声明的变量赋值
  var foo = 'foo'
})()

作用域

JavaScript 的语法来源于 C,因此也具有块语法。比如 if 后面可以使用花括号的形式将多条语句组成一个代码块,理论上来说在这个块中声明的变量对外部应该是不可见的。然而在 JavaScript 的代码块中并不是。也就是说 JavaScript 采用了这样的块语法,却没有提供块级作用域。这让有其他编程语言经验的程序员们大跌眼镜。

ES6 问世之后出现了 let 关键词声明变量的操作,使用 let 可以避免这种错误。因此咱们在实际的开发中应该总是使用 let 代替 var

自动插入分号

JavaScript 有一个机制,它总是试图自动插入分号来修正有缺陷的程序。但千万不要依赖它,这可能会掩盖更为严重的错误。因为它有时候会不合时宜的插入分号。比如:

function genObj() {
  return 
  {
    status: 'ok'
  }
}

这看上去是要返回一个包含 status 成员的对象,然而实际上是返回了一个 undefined 。因为 JavaScript 这种自动插入分号的机制在 return 之后加入了一个分号,而且运行这行代码却也并没有报错提示。如果把花括号的 { 放在与 return 一行的位置就能避免这种问题:

function genObj() {
  return {
    status: 'ok'
  }
}

这说明并不是在一行的末尾加上分号,却也并不能说明在本行有未闭合的一对标点时而不自动插入分号,咱们拿上面的自执行函数看一下:

var a = (function (){
  return 'a'
})()
​
()  // common.js:5 Uncaught TypeError: (intermediate value)(...) is not a function

立马就报错了,咱们的本意是想让立即执行该函数调用后的返回值赋给 a 变量,之后再进行其他的操作,但 JavaScript 却没在第三行末尾插入分号,而是认为这应该是持续调用。为了验证咱们的想法,咱把这个立即调用函数的返回值改为一个函数:

var a = (function (){
  return function() {
    console.log(1)
    return 'a'
  }
})()
​
()

此时就没有报错,并打印出了 1 。因此在实际的开发中,咱们应该总是加上分号。但由于没有分号看上去十分简洁,也有不少开发人员不写分号。像 Vue 源码就没有分号,因此咱们只需要在某些特定的 JavaScript 自动插入分号容易出错的地方手动加上分号即可。

咱们只需要记得在 “+”、“-”、“*”、“/”、“(”、“[” 这些符号开头之前加上分号即可。

typeof

typeof 运算符返回一个用于识别其运算数类型的字符串。比如:

typeof 98     // number
typeof 'q'    // string

但糟糕的是:typeof null 返回的竟然是 object 而并非 null。而更变态的是,检测 null 的方式却很简单:

val === null

因此,检测对象的类型时,typeof 便不能在 nullobject 之间返回一个更准确的类型字符串,所以在检测 object 类型之前需要判断是否为 null 值:

if(val !== null && typeof val === 'object') {
  // do something
}

加号运算符

加号运算符通常是产生 bug 的常见来源。他可以用于加法运算,也可以用于字符串的拼接,可他究竟会执行成怎样的结果完全取决于参数否的类型。如果其中一个运算数是字符串,他会将另一个参数也转换为字符串进行运算并返回。只有两个运算数都是数字才进行求和运算,否则都会将两个运算数转为字符串进行拼接。

如果你打算使用 + 运算符去做加法运算,一定要确保两个运算数都是数值类型。否则会出现不可预期的 bug 。

=====

===== 就像一对邪恶的孪生兄弟。前者比较两个运算数表示的值是否相等,而对其类型却不进行比较。后者属于严格比较,在比较值的同时会先进行类型匹配。如果两个运算数是不同类型时,他们试图去强制转换其值的类型,而且转换的规则复杂难以记忆。这里有一些有趣的例子:

console.log('' == 0)          // true
console.log(0 == '')          // true
console.log('0' == 0)         // true
console.log(false == '0')     // true
console.log([] == ![])        // true
console.log(![] == ![])       // true

因此在开发中总是使用 === 可以避免许多不可预期的 bug ,除非你准确的知道两边的运算数是相同类型,比如在使用 typeof 的时候:

typeof 'a' == 'string'

总结

由于 JavaScript 的诞生背景,才有了许多奇奇怪怪的特性。以上只是这些特性中的冰山一角。他们被称为卧龙凤雏豪不夸张,但这丝毫不影响它成为流行语言的趋势。经过近年来 web 的蓬勃发展,人们已经有了规避他们的一套解决方案。甚至在前进的道路上进行不断的修补, JavaScript 已然成为一把称心如意的宝剑。

加之 typescript 以及一些辅助工具(如 ESLint)的出现,JavaScript 生态一片繁荣,其统治世界指日可待,一切能用 js 实现的终将用 js 实现(开个玩笑,王者峡谷没有菜鸡的英雄,只有菜鸡的召唤师)。

另外:文中若有表述不妥或是知识点有误之处,欢迎留言批评指正,共同进步!