AICon议程上新60%,阿里国际、360智脑、科大讯飞、蔚来汽车分享大模型探索与实践 了解详情
写点什么

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

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

关注

评论

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

直播预约|Search for Future,阿里云 × Elastic 中国用户峰会 2023

阿里云大数据AI技术

大数据 阿里云 搜索

2023年适用于Windows和Mac的FTP传输工具

镭速

C++ sort和for_each算法的普通、文艺和2B用法

老王同学

c++ 排序

构建一个可复用的自定义BaseAdapter

梦笔生花

Adapter viewholder item

EasyRecovery16数据恢复软件有哪些新功能?

茶色酒

EasyRecovery Photo16

网易云信 Crash 异常治理实践 | 智企技术委员会技术专题系列

网易云信

系统设计 异常处理

预售登上计算机新书热卖榜TOP1,开年重磅,助力Java程序员飙升核心技能

图灵社区

Spring boot starter test java 后端、 程序员 java

九科祝福丨人生不设限,愿你自由绽放

九科Ninetech

Kubernetes 部署主从结构的 MySQL 服务

CTO技术共享

GitHub破千Star!Java多线程编程实战指南:核心篇+设计模式篇

做梦都在改BUG

Java 并发编程 多线程

软件测试/测试开发 | Spring Boot 异常处理

测试人

软件测试 springboot 自动化测试 测试发开

MySQL 底层之 MVCC、回滚段、一致性读、锁定读

程序知音

Gartner首次针对中国市场发布产业数字化白皮书,联合卡奥斯共探区域经济发展最优解

Openlab_cosmoplat

数字化 产业数字化 白皮书 开源社区 Gartner

【经验分享】电路板上电就挂?新手工程师该怎么检查PCB?

华秋PCB

工程师 电路 PCB PCB设计

工赋开发者社区 | 关于ChatGPT八个技术问题的猜想

工赋开发者社区

CorelDRAW矢量图形设计软件2023最新版本功能介绍

茶色酒

CorelDraw2023

Redis缓存知识大集合

阿呆

redis 缓存 缓存击穿 缓存雪崩

如何让SpringBoot项目启动时执行特定代码

做梦都在改BUG

Java Spring Boot

SpringApplication启动类的Args详解

石臻臻的杂货铺

spring springboot

软件测试/测试开发 | 数据持久化技术(Java)

测试人

软件测试 测试发开

如何降低 Flink 开发和运维成本?阿里云实时计算平台建设实践

Apache Flink

大数据 flink 实时计算

字典数据结构 FST(Finite State Transducer)

alexgaoyh

Java Trie FST dat 字典数据结构

三天吃透Kafka面试八股文

程序员大彬

Java Kafka Producer

黑盒测试方法—等价类划分法

测吧(北京)科技有限公司

测试

预售登上计算机新书热卖榜TOP1,开年重磅,助力Java程序员飙升核心技能

图灵教育

Spring boot starter test java 后端、 程序员 java

那些年,我们写过的无效单元测试

阿里巴巴中间件

阿里云 编程 云原生

我在京东做研发丨【混合多云第一课】为何多云多活被称为“技术皇冠上的明珠”?

京东科技开发者

京东云 研发 混合多云

Dubbo 在 Proxyless Mesh 模式下的探索与改进

阿里巴巴中间件

阿里云 云原生 dubbo

一个可以早点下班的开发技巧

引迈信息

前端 敏捷开发 低代码 JNPF

ERP和MES如何做到优势互补,它们的区别在哪?

工赋开发者社区

Centos7下安装Dogtail GUI自动化测试工具并打开sniff工具过程中遇到的问题解决方法

Python centos 自动化测试 sniff dogtail

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