QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

来自 1000 多个项目的 10 大 JavaScript 错误浅析

  • 2018-02-19
  • 本文字数:5011 字

    阅读完需:约 16 分钟

作为对社区开发者的回馈,我们从我们的数据库里选出了 10 大来自数千个项目的 JavaScript 错误。我们将会给出产生这些错误的根源,以及如何避免再发生这些错误。如果能够避免这些错误,就可以成为更好的开发者。

数据才是王道,我们通过收集和分析大量数据才选出了这 10 大 JavaScript 错误。我们收集每一个项目中出现的错误,并统计每一个错误发生的次数。我们根据错误代码的指纹(fingerprint)对它们进行分组,也就是说,如果第二个错误与第一个是重复的,就把它们归入同一个组。这样就可以为用户提供更好的视图,而不是像查看繁琐的日志文件那样。

我们只关注影响面最大的那些错误。为此,我们统计了错误在各个公司的项目中发生的次数,而不是错误发生的总次数,因为如果是这样的话,读者就可能看到大量与他们不相干的统计信息。

以下是排名靠前的 10 大 JavaScript 错误:

出于可读性方面的考虑,每个错误的描述经过精简。

1.Uncaught TypeError: Cannot read property

如果你是一名 JavaScript 开发者,对这个错误可能已经熟视无睹。在 Chrome 里读取未定义对象的属性或调用未定义对象的方法时就会发生这个错误,在 Chrome 开发者控制台可以很容易地重现这个错误。

发生这个错误的原因有很多,其中最为常见的是,在渲染UI 组件时没有正确初始化状态。我们通过一个真实的例子来看看这个错误是怎么发生的。我们选择React 作为示例,不过在其他框架(Angular、Vue 等)中也是一样的。

复制代码
class Quiz extends Component {
 componentWillMount() {
  axios.get('/thedata').then(res => {
   this.setState({items: res.data});
  });
 }
 render() {
  return (
   <ul>
    {this.state.items.map(item =>
     <li key={item.id}>{item.name}</li>
    )}
   </ul>
  );
 }
}

这里要注意两件事:

  1. 组件的状态(如 this.state)在一开始就是 undefined。
  2. 如果是通过异步的方式来加载数据,那么在数据加载进来之前,至少要渲染一次组件——不管是在构造器、componentWillMout() 还是 componentDidMout() 中加载数据。Quiz 在进行第一次渲染时,this.state.items 是 undefined,那么 ItemList 就会得到 undefined 的数据项,这样就会在控制台看到这个错误——“Uncaught TypeError:Cannot read property ‘map’ of undefined”。

要解决这个问题其实很简单,在构造器里使用适当的默认值进行初始化。

复制代码
class Quiz extends Component {
 // 增加这个:
 constructor(props) {
  super(props);
  // 使用空数组给 state 赋值
  this.state = {
   items: []
  };
 }
 componentWillMount() {
  axios.get('/thedata').then(res => {
   this.setState({items: res.data});
  });
 }
 render() {
  return (
   <ul>
    {this.state.items.map(item =>
     <li key={item.id}>{item.name}</li>
    )}
   </ul>
  );
 }
}

2. TypeError: ’undefined’ is not an object

在 Safari 里读取未定义对象的属性或调用未定义对象的方法时就会发生这个错误,在 Safari 开发者控制台可以很容易地重现这个错误。这个错误与发生在 Chrome 里的是差不多的,只是 Safari 为它提供了不同的错误信息。

3. TypeError: null is not an object

在 Safari 里读取空(null)对象的属性或调用空对象的方法时就会发生这个错误,在 Safari 开发者控制台可以很容易地重现这个错误。

有意思的是,在JavaScript 里,null 和undefined 其实是不一样的,所以我们会看到两个不同的错误消息。undefined 表示未赋值的变量,而null 表示变量值为空。可以使用严格等于号来证明它们不是同一个东西。

在实际应用当中,在JavaScript 里调用一个未加载的DOM 元素就会出现这个错误。如果对象为空,DOM API 就会返回null。

DOM 元素需要在创建之后才能被访问。JavaScript 代码是按照从上到下的顺序进行解析的,所以,如果在 DOM 元素之前有一个标签包含了 JavaScript 代码,浏览器在解析 HTML 时就会执行这些代码。在加载 JavaScript 之前,如果 DOM 元素没有被创建,就会出现这个错误。

在这个例子里,我们可以通过添加一个事件监听器来解决这个问题,在页面加载完毕时,事件监听器会通知我们。在 addEventListener 被触发之后,init() 方法就可以大胆地访问 DOM 元素了。

复制代码
<script>
 function init() {
  var myButton = document.getElementById("myButton");
  var myTextfield = document.getElementById("myTextfield");
  myButton.onclick = function() {
   var userName = myTextfield.value;
  }
 }
 document.addEventListener('readystatechange', function() {
  if (document.readyState === "complete") {
   init();
  }
 });
</script>
<form>
 <input type="text" id="myTextfield" placeholder="Type your name" />
 <input type="button" id="myButton" value="Go" />
</form>

4. (unknown): Script error

跨域的未捕捉 JavaScript 异常会变成 Script error。例如,假设 JavaScript 托管在 CDN 上,那么未捕捉的错误(错误没有在 try-catch 里被捕获,一路直上到了 window.onerror 里)就会显示成“Script error”,而不是显示具体的错误消息。这是浏览器出于安全方面的考虑,防止跨域传递数据。

要想获得具体的错误信息,可以这样做:

1). 使用 Access-Control-Allow-Origin

将 Access-Control-Allow-Origin 设置成“*”,表示该资源可以被任何一个域访问。如果有必要,可以把“*”替换成你的域名,例如 Access-Control-Allow-Origin: www.example.com。不过,如果使用了 CDN,那么要支持多个域名可能就会得不偿失,因为 CDN 存在缓存问题。

下面是在各种环境如何设置该字段的示例:

Apache

在 JavaScript 文件所在的目录创建一个叫作.htaccess 的文件,并加入如下内容:

Header add Access-Control-Allow-Origin “*"Nginx

在 JavaScript 对应的 location 配置代码块中加入 add_header 指令:

复制代码
location ~ ^/assets/ {
  add_header Access-Control-Allow-Origin *;
}

HAProxy

在 JavaScript 文件对应的 backend 配置块中加入如下内容:

rspadd Access-Control-Allow-Origin:\ *2). 在 script 标签里设置 crossorigin=“anonymous”

在每个设置了 Access-Control-Allow-Origin 字段的 HTML 页面里,将它们的 script 标签的 crossorigin 属性设置为“anonymous”。在 Firefox 里,如果出现了 crossorigin,但没有设置 Access-Control-Allow-Origin,JavaScript 脚本就不会被执行。

5. TypeError: Object doesn’t support property

在 IE 里读取未定义对象的属性或调用未定义对象的方法时就会发生这个错误,在 IE 开发者控制台可以很容易地重现这个错误。

这个错误与Chrome 里的“TypeError: ‘undefined’ is not a function”是同一个东西。不同的浏览器为相同的错误提供的错误消息可能是不一样的。

在IE 里使用JavaScript 的命名空间时,就很容易碰到这个错误。发生这个错误十有八九是因为IE 无法将当前命名空间里的方法绑定到this 关键字上。例如,假设有个命名空间Rollbar,它有一个方法叫isAwesome()。在Rollbar 命名空间中,可以直接使用this 关键字来调用这个方法:

this.isAwesome();在 Chrome、Firefox 和 Opera 中这样做都是没有问题的,但在 IE 中就不行。所以,最安全的做法是指定全命名空间:

Rollbar.isAwesome();### 6. TypeError: ‘undefined’ is not a function

在 Chrome 里调用一个未定义的函数时就会发生这个错误,可以在 Chrome 开发者控制台和 Mozilla 开发者控制台重现这个错误。

近年来,JavaScript 的编码技术和设计模式变得日趋复杂,回调和闭包中的自引用情况越来越普遍,让人搞不清楚代码中的this/that 表示的是什么意思。

比如下面这段代码:

复制代码
function testFunction() {
 this.clearLocalStorage();
 this.timer = setTimeout(function() {
  this.clearBoard();  // 这里的”this" 是指什么?
 }, 0);
};

执行上面的代码会出现这样的错误:“Uncaught TypeError: undefined is not a function”。因为在调用 setTimeout() 方法时,实际上是在调用 window.setTimeout()。传给 setTimeout() 的匿名函数的上下文实际上是 window,而 window 并不包含 clearBoard() 方法。

对于旧浏览器,以往的解决办法是将 this 赋值给某个变量,然后在闭包里使用这个变量。例如:

复制代码
function testFunction () {
 this.clearLocalStorage();
 var self = this;  // 将 this 赋值给 self
 this.timer = setTimeout(function(){
  self.clearBoard();  
 }, 0);
};

在新浏览器中,可以使用 bind() 方法来传递引用:

复制代码
function testFunction () {
 this.clearLocalStorage();
 this.timer = setTimeout(this.reset.bind(this), 0); // 绑定到 'this'
};
function testFunction(){
  this.clearBoard();  // 以’this’作为上下文
};

7. Uncaught RangeError: Maximum call stack

在 Chrome 里,有几种情况会发生这个错误,其中一个就是无限递归调用一个函数。这个错误可以在 Chrome 开发者控制台重现。

当传给函数的值超出可接受的范围时也会出现这个错误。很多函数只接受指定范围的数值,例如,Number.toExponential(digits) 和Number.toFixed(digits) 只接受0 到20 的数值,而Number.toPrecision(digits) 只接受1 到21 的数值。

复制代码
var a = new Array(4294967295); //OK
var b = new Array(-1); //range error
var num = 2.555555;
document.writeln(num.toExponential(4)); //OK
document.writeln(num.toExponential(-2)); //range error!
num = 2.9999;
document.writeln(num.toFixed(2));  //OK
document.writeln(num.toFixed(25)); //range error!
num = 2.3456;
document.writeln(num.toPrecision(1));  //OK
document.writeln(num.toPrecision(22)); //range error!

8. TypeError: Cannot read property ‘length’

在 Chrome 里读取 undefined 变量的 length 属性时会发生这个错误,这个错误可以在 Chrome 开发者控制台重现。

length 是数组的属性,但如果数组没有初始化或者数组的变量名被另一个上下文隐藏起来的话,访问 length 属性就会发生这个错误。例如:

复制代码
var testArray= ["Test"];
function testFunction(testArray) {
  for (var i = 0; i < testArray.length; i++) {
   console.log(testArray[i]);
  }
}
testFunction();

函数的参数名会覆盖全局的变量名。也就是说,全局的 testArray 被函数的参数名覆盖了,所以在函数体里访问到的是本地的 testArray,但本地并没有定义 testArray,所以出现了这个错误。

有两种方法可用于解决这个问题:

1). 将函数的参数名移除(这就表示函数里要访问的变量已经在函数外面定义好了,所以函数不需要参数):

复制代码
var testArray = ["Test"];
/* 前提是要在函数外面定义好 testArray */
function testFunction(/* No params */) {
  for (var i = 0; i < testArray.length; i++) {
   console.log(testArray[i]);
  }
}
testFunction();

2). 在调用函数时将变量传递进去:

复制代码
var testArray = ["Test"];
function testFunction(testArray) {
  for (var i = 0; i < testArray.length; i++) {
   console.log(testArray[i]);
  }
}
testFunction(testArray);

9. Uncaught TypeError: Cannot set property

我们无法对 undefined 变量进行赋值或读取操作,否则的话会抛出“Uncaught TypeError: cannot set property of undefined”异常。

例如,在 Chrome 中:

如果test 对象不存在,就会抛出“Uncaught TypeError: cannot set property of undefined”异常。

10. ReferenceError: event is not defined

在访问一个未定义的对象或超出当前作用域的对象时就会发生这个错误,这个错误可以在 Chrome 开发者控制台重现。

如果在进行事件处理时遇到这个错误,请确保事件对象被作为参数传入到函数当中。旧浏览器(IE)提供了全局的event 变量,但并不是所有的浏览器都会这样。尽管jQuery 尝试对这种行为进行规范化,但最好还是使用传给函数的event 对象:

复制代码
function myFunction(event) {
  event = event.which || event.keyCode;
  if(event.keyCode===13){
    alert(event.keyCode);
  }
}

结论

我们希望这些内容能够帮助大家在未来避免这些错误,解决大家的痛点。不过,即使有了这些最佳实践,在生产环境中仍然会出现各种不可预期的错误。关键是要及时发现那些影响用户体验的错误,并使用适当的工具快速解决这些问题。

查看英文原文 Top 10 JavaScript errors from 1000+ projects (and how to avoid them)

感谢徐川对本文的审校。

2018-02-19 17:102576
用户头像

发布了 322 篇内容, 共 146.3 次阅读, 收获喜欢 148 次。

关注

评论

发布
暂无评论
发现更多内容

CPU利用率从10%提升至60%:中型企业云原生成本优化实战指南

星汉未来

运维 云原生 IT成本 星汉未来 FinOps

成本节省 50%,10 人团队使用函数计算开发 wolai 在线文档应用

Serverless Devs

Serverless wolai

“东数西算”与“双碳”双驱力叠加,新华三争当“全能型选手”

BeeWorks

Redis 忽然变慢了如何排查并解决?

码哥字节

redis Redis 核心技术与实战 6月月更

运维领域告警智能定级原理探索(含详细实验报告)

云智慧AIOps社区

运维 安全 监控 告警

如何撰写数据中台蓝图方案

agileai

数据中台 企业服务总线 主数据平台 数据分析平台 蓝图方案

2022云原生网络趋势 | K8s托管整个基础设施、多云、边缘计算、安全等场景,将云原生网络带向新战场

York

云原生 网络 Kube-OVN cni 6月月更

有了这个云端Mock功能,你的简历起码提升30分!

Liam

前端 Postman 前端教程 API文档 前端工具

6 月直播 7 场干货全剧透!今天:飞腾CPU调优原理及方法 | 第 19 期

OpenAnolis小助手

cpu 直播 sig 龙蜥大讲堂 飞腾

各国儿童节时间是不一样的

清林情报分析师

数据可视化 知识图谱 儿童节

【高并发】你知道吗?大家都在使用Redisson实现分布式锁了!!

冰河

并发编程 多线程 高并发 异步编程 6月月更

洗车行业前景好不如开个自助洗车店

共享电单车厂家

自助洗车加盟 开自助洗车店

6元自助洗车机一般都什么价位

共享电单车厂家

自助洗车加盟 6元自助洗车机

为企业业务流程提速的BPM

力软低代码开发平台

2022年4月线上终端药品增长迅猛,市场政策合规进程加快

易观分析

医药类

英特尔计划建造浸没式实验室,帮助高功率芯片快速降温

BeeWorks

Jetpack Composes 入门

坚果

6月月更

为什么你的网站需要搭建在线帮助中心?

小炮

博睿数据拨测入场加速广电深度融合

博睿数据

智能运维 博睿数据 智慧广电

顶级好用的 React 表单设计生成器,可拖拽生成表单

蒋川

低代码 开发工具 React 表单 组件

哪些人比较适合加盟自助洗车

共享电单车厂家

加盟自助洗车

自助洗车机还能加盟你不知道吧?

共享电单车厂家

自助洗车机 自助洗车加盟

相约龙蜥,开源一“夏”!2022编程之夏ASoC开始报名了

OpenAnolis小助手

阿里巴巴 开源项目 龙蜥社区 高校学生 技术项目

招聘 | 上班轰趴,下班狼人杀,天天招人,怕是要发!

Alluxio

面试 程序员人生 招聘 互联网热点 Alluxio

幸运哈希算法竞猜游戏开发特点分析(成熟方案)

开发微hkkf5566

展示 Postlight 的 WordPress + React Starter Kit

海拥(haiyong.site)

WordPress 6月月更

WASM VS EVM,波卡的选择预示了公链未来

One Block Community

区块链 公链 波卡生态

2022 支付宝五福 |“联机版”打年兽背后的网络技术 RTMS

阿里巴巴终端技术

客户端 网络技术 网络通信

定档615 | 数字化基础软件自主创新分享周即将来袭,点击获取“通关密钥”!

网易数帆

大数据 云原生 基础软件 数字化转型 自主创新

移动端异构运算技术-GPU OpenCL编程(进阶篇)

百度Geek说

24小时无人自助洗车要如何加盟?

共享电单车厂家

自助洗车加盟

来自1000多个项目的10大JavaScript错误浅析_JavaScript_Rollbar_InfoQ精选文章