写点什么

来自 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:102454
用户头像

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

关注

评论

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

一小时内,构建出属于你自己的 Perplexity AI 搜索引擎

智领云科技

开源 实操 Python 代码 Perplexity AI 搜索引擎

项目管理这些问题,你是不是忍了很久?

天津汇柏科技有限公司

项目管理 低代码开发 软件定制开发

软件测试学习笔记丨Selenium学习笔记:元素定位与操作

测试人

软件测试

阿里大牛纯手打“亿级高并发系统设计手册”(限时开源)

架构师之道

高并发 java面试

用户身份与访问管理(IAM)是数字身份管理的关键路径和重要方法

芯盾时代

数字身份 iam 统一身份管理平台

远程桌面控制软件Microsoft Remote Desktop for Mac

Mac相关知识分享

桌面控制软件

Xmind for Mac(思维导图软件)中文版

Mac相关知识分享

Android File Transfer for mac(强大的安卓文件传输工具)

Mac相关知识分享

Photoshop 2020 for Mac(PS 2020)

Mac相关知识分享

ps

全域旅游平台(源码+文档+部署+讲解)

深圳亥时科技

数据为王,实时监控显神威 —— 淘宝商品详情API的电商实战应用

技术冰糖葫芦

API 接口 API 文档 API 测试 pinduoduo API

BOE(京东方)携手雷神联合发布全球首款仿生蜂鸟屏 以全新升级ACR技术引领显示产业高端化的升维发展

科技汇

火山引擎数智平台VeDI荣获2024爱分析·数据智能优秀厂商奖

字节跳动数据平台

大数据 数据飞轮

揭秘 IT 成熟度模型:助力企业在数字时代脱颖而出的战略工具

跟YY哥学Jira

ITSM Gartner 成熟度模型

要低代码,但不要低能力,低代码产品能否成为企业的增效神器?

优秀

低代码 低代码平台 低代码平台比较

糟糕,当我上班用deepin,被老板看到了我的桌面后......!

nn-30

Linux 操作系统 deepin 桌面应用 桌面

数据驱动未来:淘宝商品详情API在电商实战中的实时监控力量

代码忍者

API 接口 pinduoduo API

智联招聘×Milvus:向量召回技术提升招聘匹配效率

Zilliz

人工智能 AI Milvus Zilliz 向量数据库

好用的运维安全审计系统需满足哪些要求或者诉求?

行云管家

运维 安全运维 运维审计

易于使用的视频下载工具Downie 4 for Mac

Mac相关知识分享

视频下载工具

PDF专业制作与编辑软件Acrobat Pro DC 2021 for Mac

Mac相关知识分享

PDF编辑软件

Microsoft 365 for Mac(原Office 365)

Mac相关知识分享

办公软件

CST软件如何选择时域求解器的频率范围

思茂信息

仿真软件 cst 电磁仿真

2024年西藏等保测评机构名单汇总

行云管家

等保 等保测评 西藏

Altair官方文档——HyperMesh模型管理

智造软件

教程 CAE软件 altair Hypermesh

公开课 | AI赋能自动化测试:解锁未来测试新篇章

测试人

人工智能 软件测试

MariaDB 和 GreatSQL 性能差异背后的真相

GreatSQL

专业视频编辑软件Final Cut Pro for Mac

Mac相关知识分享

视频编辑软件

矢量编辑工具Sketch for mac(矢量绘图软件)

Mac相关知识分享

1024茶思屋直播|TinyEngine跨端实践​:可视化搭建导出鸿蒙ArkTs应用

OpenTiny社区

低代码 OpenTiny 前端开源

CNCC | 倒计时3天!CCF-网易雷火联合基金研讨会:议程嘉宾交通参会指南一图掌握

网易伏羲

人工智能 网易伏羲 cncc 具身智能 群体智能

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