HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

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

关注

评论

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

Yuliverse:引领区块链游戏新篇章!

Footprint Analytics

区块链游戏 元宇宙 链游 Web3 游戏

惊为天人!这个保存推特视频到手机相册的办法真的一绝!

frank

twitter

CleanMyMac X v4.14.6中文版 强大的mac系统清理工具

iMac小白

DirEqual mac 5.8 激活版 文件夹比较工具

iMac小白

基于taro搭建小程序多项目框架

EquatorCoco

小程序 taro 前端框架 小程序开发

走出大模型部署新手村!小明这样用魔搭+函数计算

Serverless Devs

容器 中间件 函数计算 函数计算FC

魔搭+ 函数计算: 一键部署,缩短大模型选型到生产的距离

Serverless Devs

容器 云原生 中间件 函数计算 函数计算FC

小程序全生命周期管理如何安排

Geek_2305a8

Sketch for mac(矢量绘图软件) 99.1中文激活版

iMac小白

速看!盘点这一年 OpenTiny 都在做什么?

OpenTiny社区

开源 前端 低代码 组件库

MetaVideo mac 1.1.3激活版 视频元数据编辑

iMac小白

应用集成(iPaaS)和数据集成(ETL)高效协同的最佳实践

RestCloud

ETL 数据集成 应用集成 ipaas

部署SD-WAN需要多长时间?多少钱?

Ogcloud

SD-WAN SD-WAN组网 SD-WAN服务商

测试管理进阶 | 如何打造一份出彩的工作汇报!

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

测试

定向减免!函数计算让 ETL 数据加工更简单

Serverless Devs

容器 云原生 中间件 函数计算 函数计算FC

Apifox 1月更新:性能测试、多人协作接口实时同步功能全面解析

Apifox

程序员 性能测试 Apifox API 接口工具

2023 年度龙蜥最佳用户案例奖揭晓,中国移动、小红书、中国人寿财险等企业上榜!

OpenAnolis小助手

开源 操作系统 用户案例 龙蜥社区 商业版

玩转数据处理利器:学会使用 YAML 文件轻松处理数据

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

测试

2023京东零售技术年度盘点

京东科技开发者

软件测试学习笔记丨抓包工具Charles实战

测试人

软件测试

Web3 游戏开发者的数据分析指南

Footprint Analytics

区块链 区块链游戏 #Web3 web3游戏

Disk Drill mac 5.4.1426中文激活版 数据恢复软件

iMac小白

Capture One 23 Enterprise Mac 16.3.4.5企业版 RAW图像编辑处理

iMac小白

华为云幻兽帕鲁服务器搭建教程(Windows平台)

YG科技

理解ETF的风险:透明度高、投资组合清晰可见背后的考量

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

业务-研发一体化的管理平台是否存在?

Geek_2305a8

重磅来袭“2024粤港澳电子展”覆盖电子信息完整产业链

AIOTE智博会

电子展 电子信息展 电博会

低代码(Low-Code)技术简化开发难度,快速搭建应用

互联网工科生

软件开发 低代码 数字化 JNPF

OpenKruise :Kubernetes背后的托底

华为云开发者联盟

开发 华为云 华为云开发者联盟

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