NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

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

关注

评论

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

沉铜/黑孔/黑影工艺,PCB该选哪一种?

华秋PCB

工艺 PCB PCB设计

中大型企业组织为什么要选择私有化企业IM?

WorkPlus

airserver2023免费中文版下载

茶色酒

AirServer2023

行业分析| 智慧头盔在快对讲上的应用与实践

anyRTC开发者

音视频 智能设备 视频通话 快对讲 智慧头盔

优化 20% 资源成本,新东方的 Serverless 实践之路

Serverless Devs

Serverless

新一代云原生实时数仓 SelectDB 重磅发布!最全技术亮点解读都在这里了

SelectDB

数据库 云计算 大数据 数据仓库 云原生

OpenAI掌门人Sam Altman:AI的下一个发展阶段

OneFlow

人工智能 深度学习 openai

顶级大咖齐聚云端,赛意信息这场数字化盛宴不容错过

科技热闻

2021最新百度Java岗四轮面试题,(三轮技术面+一轮HR面)

钟奕礼

Java 程序员 java面试 java编程

2022高通人工智能开发者大会暨高通人工智能应用创新大赛颁奖典礼圆满落幕

科技热闻

APISIX Ingress 如何支持自定义插件

API7.ai 技术团队

Kubernetes 云原生 api 网关 APISIX Ingress Controller

阿里三面,讲讲不同场景下并发Map容器最优使用。凉凉送给自己

钟奕礼

Java 程序员 Java 面试 java编程

最新2021快手面试Java岗面经,成功手拿offer,开心到飞

钟奕礼

Java 程序员 Java 面试 java编程

为什么新能源汽车上的IGBT会如此火爆?

元器件秋姐

半导体 新能源汽车 新能源 汽车芯片 功率半导体

激活海量数据价值,实现生产过程优化|智慧工厂系列专题07

EMQ映云科技

人工智能 物联网 IoT 云边协同 12 月 PK 榜

Milvus 2.1.x 到 Milvus 2.2.x 升级实践

Zilliz

数据库 Milvus

2021最强网易Java岗面试题,(实战篇)进大厂必备~不看后悔

钟奕礼

Java 程序员 java面试 java编程

互联网时代,云计算的 6 大特征

Finovy Cloud

云计算 云渲染

企业安全的移动办公平台应该具备哪些能力?

WorkPlus

BaseDet: 走过开发的弯路

MegEngineBot

深度学习 开源 目标检测 MegEngine BaseDet

FLStudio21新版有哪些新功能推荐?

茶色酒

FLStudio21 FLStudio21.0.0

GPR政务流程再造赋能数字政府建设

张磊

人工智能 大数据 数字政务 流程再造 BPR

英特尔梁雅莉:拥抱数字时代,满足日益增长的算力需求

科技之家

最近大火的高性能计算ChatGPT究竟是什么?

蓝海大脑GPU

深度学习 高性能计算, ChatGPT

ThingsBoard前端项目的安装与启动

echeverra

thingsboard

节能降耗 | AIRIOT智慧电力综合管理解决方案

AIRIOT

物联网 智慧电力

处理中国式报表,一款合适的BI商业智能工具尤为重要

小偏执o

CorelDRAW2023离线下载安装步骤

茶色酒

CorelDraw2023

知乎疯转30K+的微服务架构笔记,理论与实战齐飞!

小小怪下士

Java 微服务 架构设计

新闻速递 | KaiwuDB 亮相 DTCC 2022 线上大会并发表系列主题演讲

KaiwuDB

数据库

socket是并发安全的吗

C++后台开发

网络编程 socket 多线程 TCP/IP Linux服务器开发

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