4

JavaScript常出现的错误前十位

clipboard.png

为了可读性,错误名称进行了一定的简写。让我们深入了解每个错误发生的原因以及解决方法。

1. Uncaught TypeError: Cannot Read Property

如果你是一名JavaScript开发人员,你可能已经记不清楚多少次看到这个错误了。当你读取一个undefined对象的属性或是调用其上的方法时,就会出现这个错误。你可以再Chrome Console中进行测试。
clipboard.png
导致这个问题的原因有许多,最常见的是渲染UI组件时对state不恰当的初始化。让我们看一个真实APP中可能出现该情况的例子。我们选择了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. 组件的state(比如 this.state)在生命周期开始时为undefined。
  2. 当你异步获取数据的时候,component会在数据加载之前至少渲染一次 - 无论是否在constructor中获取数据,都会运行componentWillMount或是componentDidMount。当Quiz第一次渲染的时候,this.state.items为undefined。因此,item列表获得的值为undefined,因此会报错"Uncaught TypeError: Cannot read property ‘map’ of undefined"

这个问题很容易解决。最简单的方法是,在构造器里面将state初始化为一个合理的默认值。


class Quiz extends Component {
  // Added this:
  constructor(props) {
    super(props);
    // Assign state itself, and a default value for items
    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 (evaluating...)

这是一个在Safari中在undefined对象上访问属性或方法时报的错。你可以在Safari的控制台上进行测试。这个错误和之前在Chrome中出现的错误是相同,只是报错信息不同。
clipboard.png

3. TypeError: Null Is Not an Object (evaluating...)

这是在Safari中在访问null对象上的属性或方法时报的错。
clipboard.png

有趣的是,在JavaScript中,null和undefined是不同的,所以我们看到了两个不同的报错信息。Undefined通常是指一个尚未赋值的变量,而null是指该变量的值为空。要想判断二者不等,应当使用严格的相等操作符:
clipboard.png

在真实世界中,这种错误可能出现的原因之一是你试图在元素加载完成之前访问DOM元素。对于空白的对象引用,DOM API会返回null。

任何对DOM元素进行处理的JS代码都应该都在DOM元素创建完成之后进行。JS代码按照HTML中的规定按从上到下的顺序进行解释。所以,如果在DOM元素之前存在标签,则脚本标签内的JS代码将在浏览器解析HTML页面时执行。如果在加载脚本之前尚未创建相关的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错误跨越违法跨域策略的域边界时,会发生脚本错误。比如,如果你将你的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,那么这样子的代价可能不值得。详情参考这里

这里给出一些在不同的环境中设置header的例子:
Apache
在你存放JavaScript的文件夹中添加一个.htacess文件,包含以下内容:

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

Nginx
将add_header指令添加到为JavaScript文件提供服务的位置块:

location ~ ^/assets/ {
    add_header Access-Control-Allow-Origin *;
}

HAProxy
将以下内容添加到提供JavaScript的asset backend

rspadd Access-Control-Allow-Origin:\ *

2. 在script标签上设置crossorigin="annonymous"属性

在HTML中,对于每一个设置了Access-Control-Allow-Origin头的脚本,在脚本的标签上添加crossorigin="anonymous"属性。在将crossorigin属性添加到脚本之前,请确保验证是否为脚本文件设置了header。在火狐浏览器中,如果设置了crossorigin属性但是没有设置Access-Control-Allow-Origin头,该脚本不会执行。

5. TypeError: Object Doesn’t Support Property

这是在IE浏览器中报的错,当你试图调用一个undefined对象的方法时:
clipboard.png

这等价于Chrome中的TypeError: ‘undefined’ is not a function错误。是的,不同的浏览器对相同的错误会产生不同的报错信息。

对于使用JavaScript命名空间的Web程序,在IE上运行时经常会遇到这个错误。当这个错误出现时,99.9%的情况是因为IE不能将当前的命名空间的方法绑定到this关键字上。比如,假设你有一个JS命名空间Rollbar,其下有一个方法isAwesome()。通常在Rollbar命名空间下你会用如下的语法调用isAwesome方法:

this.isAwesome();

Chrome,Firfox和Opera都会愉快的接受这个语法。但是,IE并不会。因此,使用JS命名空间时最安全的选择是始终以实际的命名空间作为前缀。

Rollbar.isAwesome();

6. TypeError: ‘undefined’ Is Not a Function

这是当你在Chrome中试图调用undefined的方法时出现的错误。
clipboard.png

随着JavaScript的编程技巧和设计模式在这几年来越来越复杂,在回调和闭包中自我引用范围的扩散也相应的增加,导致对this出现困惑。
看下面这段代码:

function testFunction() {
  this.clearLocalStorage();
  this.timer = setTimeout(function() {
    this.clearBoard();    // what is "this"?
  }, 0);
};

运行上面的代码会出现"Uncaught TypeError: undefined is not a function."报错。原因是当你试图调用setTimeout()方法时,你实际上在调用window.setTimeout()方法。因此,一个匿名的函数传入到setTimeout()方法中,该函数的上下文实际上是window对象,而window对象没有clearBoard()方法。

一个传统的,浏览器兼容的方案是将引用this存储到一个变量中,该引用能够被闭包继承,如下:

function testFunction () {
  this.clearLocalStorage();
  var self = this;   // save reference to 'this', while it's still this!
  this.timer = setTimeout(function(){
    self.clearBoard();  
  }, 0);
};

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

function testFunction () {
  this.clearLocalStorage();
  this.timer = setTimeout(this.reset.bind(this), 0);  // bind to 'this'
};
function reset(){
    this.clearBoard();    //back in the context of the right 'this'!
};

7. Uncaught RangeError: Maximum Call Stack

这是在Chrome中出现的一种错误。情况之一是当你调用了一个没有终止的递归方法:
clipboard.png

当你向方法传了一个超越规定范围的值也可能会出现这个报错。很多方法只接受特定范围的值作为输入。比如,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属性时报的错。
clipboard.png

你通常可以在array中找到length属性,但是你也可能在array还没有初始化或是变量名被隐藏在另一个上下文中时遇到这个错误。让我们用下面这个例子理解一下这个报错:

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

当你在方法中声明参数时,这些参数成为了局部变量。这意味着即使你有名为testArray的全局变量,方法中相同名称的参数还是会被当做局部变量。

你有两种方法来结局这个问题:

  1. 删去方法声明中的参数(如果你想要访问方法外的变量,就不需要在方法参数中声明)
var testArray = ["Test"];
/* Precondition: defined testArray outside of a function */
function testFunction(/* No params */) {
    for (var i = 0; i < testArray.length; i++) {
      console.log(testArray[i]);
    }
}
testFunction();
  1. 向方法传入声明的参数
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的变量时,通常会返回undefined,而我们不能获取或是设置undefined的属性。这时候,应用就会抛出“Uncaught TypeError cannot set property of undefined.”报错。

clipboard.png

如果test对象不存在,也会抛出“Uncaught TypeError cannot set property of undefined.”

10. ReferenceError: Event Is Not Defined

当你试图访问的变量为undifined或是不在当前作用域范围内时,会抛出这个错误:
clipboard.png

如果你在使用事件处理系统时遇到这个报错,请确保你将事件对象作为参数传入了处理方法中。老的浏览器器如IE会提供一个全局的事件变量,而Chrome会自动将事件变量附属到handler上。Firfox不会自动添加它。而类似jQuery之类的库则试图规范化这个行为。总之,你最好将event作为采纳数传入事件处理方法中:

document.addEventListener("mousemove", function (event) {
  console.log(event);
})

总结

看来大多数的错误都是null或是undefined相关的错误。如果你在使用编译器的严格模式选项,一个良好的类型检查系统如Typescript能够帮助你避免这些问题。它会在一个预期类型没有被定义时警告你。即便没有Typescript, 它也能帮助我们使用防御性编程,在调用对象之前检查对象是否是undefined。

我们希望你能够学到一些新的内容,并且在未来能够避免这些错误,也可能这个指南帮你解决了一些头疼的问题。无论如何,即便是最佳实践,在编码过程中还是会出现意料之外的错误。了解影响用户使用的错误并且拥有可以快速解决问题的工具是很重要的。

clipboard.png
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~


raledong
2.7k 声望2k 粉丝

心怀远方,负重前行