前言
翻译自原文, 省去了原文的开头和结尾, 只翻译中间的主要内容介绍 10 个最常见的 js 错误, 并如何避免这些陷阱部分地方是我在翻译时, 根据自己的理解加上的标注或一些延展性的内容
这里是 10 大 JavaScript 错误:
image.png
为了可读性, 上面错误的描述都是缩写后的接下来会深入探讨一下, 这些错误发生的原理, 并且如何避免触发他们
1. Uncaught TypeError: Cannot read property
如果你是一个 JavaScript 开发人员, 可能你看到这个错误的次数, 比你希望承认的次数还要多当你在一个未定义 undefined 的对象上读取一个属性或调用一个方法时, 这个错误就会在 chrome 里触发 (当然在其他浏览器中也会报错, 但是错误信息不是这样描述的) 在 chrome 开发者控制台 console 里, 可以测试这个错误:
image.png
这个错误出现的原因有很多, 最常见的一种场景是: 当使用 UI 组件进行渲染时, 声明 state 不正确让我们来看下下面这段在真实 app 中的示例代码片段我们选取的 react 的代码, 但是同理这种不恰当的声明在 vueAngular 或其他框架中也会出现
- 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>
- );
- }
- }
这里有两点需要注意的:
一个组件的 state(比如上面的 this.state)在组件生命周期开始时是未声明的, 为 undefined
当你异步获取数据时, 组件在获取到数据之前, 无论你获取数据的代码是写在 constructor 方法, 还是 componentWillMount 或者 componentDidMount 的生命周期里, 至少都会调用一次 render 方法渲染模板上面的示例代码运行第一次 render 的时候, this.state.items 为 undefined 这意味着本该是 ItemList 的值, 却为 undefined, 接着你就会在 console 里看到一个错误 Uncaught TypeError: Cannot read property map of undefined
这个问题修复起来也很简单最简单的方法: 在 constructor 里初始化时用恰当的默认值赋值给 state
- 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>
- );
- }
- }
在你的 app 中的具体代码可能和上面有区别, 但我们还是希望这会给你足够多的线索去修复或避免这个错误如果没能帮到你, 请继续阅读下面的更多例子以及相关的错误
2.TypeError: undefined is not an object (evaluating
当你在一个未定义 undefined 的对象上读取一个属性或调用一个方法时, 在 safari 里就会报这个错你可以再 Safari 的 console 控制台里测试这个错误, 本质上和上面那个在 chrome 中出现错误是一样的, 只是在 Safari 用里的错误信息有区别
image.png
3. TypeError: null is not an object (evaluating
当你去读取一个 null 对象的属性或调用方法时, 会在 Safari 里出现这个错误可以在 Safari 控制台里测试这个错误
image.png
有趣的是, 在 JavaScript 中的 null 和 undefined 是不相等的, 所以我们才会得到不同的错误信息 Undefined 通常是指一个变量没有被声明, 而 null 表示一个变量的值为空使用严格相等操作符可以证实他们是不相等的
image.png
在实际项目中有一种出现这种错误的场景: 当你在 js 中想要操作一个 dom 元素, 但这个元素还没加载或者不存在时这是因为 dom 的 API 会在你查找 dom 元素的结果为空的情况下返回 null
任何处理 dom 元素的代码必须要放在 dom 元素被创建完毕之后 JS 代码正如 html 中一样, 是从上而下执行的所以, 如果你在 html 代码里的 dom 元素之前使用了一个 JavaScript 标签, 并在里面包含了一些内联的 js 代码, 那么这些 js 代码会在 html 页面解析之前执行这时可能就会出现这个错误, 因为在加载 js 代码之前, dom 元素还没有被创建好
在这种情况下, 我们可以通过添加一个监听页面是否解析完毕的事件监听来解决问题一但事件监听器触发, 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>
译者注:
上面说的这个问题, 是因为在 html 中所有资源的加载都是从上而下同步加载的, 所以以前的代码规范都会有一句: 在 html 里 CSS 标签放上面, js 标签放下面; 包括比如 jQuery 里的 ready 方法, 这些做法都是为了保证 js 代码执行的时候, 页面上的 dom 元素都是创建好了的
这里我再介绍一下 defer 和 async, 在外链引入 js 文件的情况, 可以在 script 标签上加上 defer 或 async 修饰符, 使该 js 能够异步加载, 从而解决上面遇到的问题 async 表示后续的解析任务和当前 js 标签的加载任务并行执行, defer 表示该 js 标签的代码会在所有页面元素解析完成之后, DOMContentLoaded 事件触发之前执行两者具体区别参考: https://segmentfault.com/q/1010000000640869
4. (unknown): Script error
当一个未被捕获的错误在跨域时, 违背了浏览器的跨域策略, 就会出现这个错误举个例子, 你把 js 代码放在了 CDN 上面, 任何未捕捉的错误发生时 (这里指冒泡到 window.onerror 的监听处理器, 而没有 try/catch 的错误) 都只会报一条简单的'Script error'信息, 而没有更加详细有帮助的错误信息这是浏览器的一种安全手段, 为了防止跨域传输数据, 不允许进行通信
想要获取到真实详细的错误信息, 你可以像这样做:
1. 在 header 里添加 Access-Control-Allow-Origin 字段
在 header(这应该是服务器返回的 response header)字段里, 把 Access-Control-Allow-Origin 设为, 这样就表示来自任意的域名请求都可以正确地访问到服务器的资源必要的话也可以指定具体的域名来代替星号, 比如:
Access - Control - Allow - Origin: www.example.com
但是配置的域名太多的话, 处理起来会有点棘手, 而且如果你在使用 CDN 的话还会出现缓存的问题, 这样就有点费力不讨好了更多参考这里
下面举一些在各种环境下配置这个 header 的示例:
Apache:
在 JavaScript 代码所在的文件夹目录下, 新建一个. htaccess 文件, 内容如下:
- Header add Access - Control - Allow - Origin "*"
- Nginx:
在 JavaScript 代码所在文件夹目录下面, 添加 add_header 命令:
- location~ ^ /assets/ {
- add_header Access - Control - Allow - Origin * ;
- }
- HAProxy:
在后端的 JavaScript 所在文件加入以下内容:
rspadd Access - Control - Allow - Origin: \ *
2. 在 JavaScript 标签上设置 crossorigin="anonymous"
在 html 代码里, 每个设置好了 Access-Control-Allow-Origin 的 js 资源, 都可以在其 JavaScript 标签上添加 crossorigin="anonymous" 在设置 crossorigin="anonymous" 之前, 确定好 header 字段都是正确发送了的在 Firefox 里, 如果 js 标签上出现了 crossorigin 属性, 但是 header 里没有 Access-Control-Allow-Origin, 那么该 js 将不会被执行(crossorigin 是 html5 新增的功能, 不只是 JavaScript 标签独有的, 比如 videoimage 也可以设置)
5. TypeError: Object doesnt support property
这个错误发生在 IE 浏览器中, 当你调用一个未定义的方法时, 可以在 IE 的 console 里测试这个:
image.png
这个错误和发生在 chrome 里的 "TypeError: undefined is not a function" 是相同的, 不同的浏览器对于相同的逻辑错误会给出不同的错误信息
这是一个常见的错误, 当你在 IE 里操作 JavaScript 的命名空间时这种情况百分之九十九是因为 IE 无法将当前作用域的方法绑定给 this 关键字举个例子, 假设你有一个名叫 Rollbar 的作用域, 里面包含了一个 isAwesome 函数正常情况下, 你可以用下面这样的语法在 Rollbar 作用域里引用 isAwesome 函数:
this.isAwesome();
Chrome,Firefox 和 Opera 会能接受这个语法, 但是 IE 不行 因此, 使用 JS 命名空间时最安全的选择是始终以实际名称空间作为前缀:
- Rollbar.isAwesome();
- 6. TypeError: undefined is not a function
调用一个未定义的函数时会出现这个错误, 可以在 Chrome 或 Mozilla Firefox 的 console 里测试这个:
image.png
随着 js 代码的编码技巧和设计模式越来越复杂, 在回调函数闭包等各种作用域中 this 的指向的层级也随之增加, 这就是 js 代码中 this/that 指向容易混淆的原因
先来看下这段代码:
- function testFunction() {
- this.clearLocalStorage();
- this.timer = setTimeout(function() {
- this.clearBoard(); // 这个 this 指向谁?
- }, 0);
- };
执行上述代码时, 会出现错误: "Uncaught TypeError: undefined is not a function." 这是因为你执行 setTimeout 方法时, 其实是执行的 window.setTimeout 所以作为参数传递过去的匿名函数, 其实是在 window 作用域下执行的, 而 window 对象并没有 clearBoard 方法
一个最简单的能兼容旧版本浏览器的方法, 就是先把 this 指向赋值给一个变量 self, 然后在闭包里直接引用这个 self 变量像这样:
- function testFunction () {
- this.clearLocalStorage();
- var self = this; // 把 this 赋值给 self, 这个作用域就会被保存下来
- this.timer = setTimeout(function(){
- self.clearBoard();
- }, 0);
- };
另外也可以使用 bind 方法来传递恰当的 this 指向:
- function testFunction() {
- this.clearLocalStorage();
- this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this'
- };
- function testFunction() {
- this.clearBoard(); //back in the context of the right 'this'!
- };
- 7.Uncaught RangeError: Maximum call stack
在 chrome 中有好几个情况会触发这个错误其中一种情况就是无终止地调用一个递归函数
image.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!
译者注:
我在 chorme 测试时, 发现上述的第二种参数超出范围的情况, 错误信息并不是 Maximum call stack, 并且
Number.toExponential(digits)
和
Number.toFixed(digits)
方法, 接收的范围应该是 0 到 100
image.png
另外, 这个错误的直译过来就是堆栈内存满了可以这样理解, 每次递归调用函数, 就会使用一层 "栈的内存", 当达到浏览器默认的堆栈大小或内存耗尽时, 就会触发这个错误那么如何防止呢? 可以尾调用优化, 函数结尾改成尾递归, 具体内容参考这里, 文中提到的一个观念就是使用尾递归来避免栈溢出, 遗憾的是目前 js 还是无法支持 "尾调用优化"
8. TypeError: Cannot read property length
当在 chorme 中读取一个未定义变量的 length 属性时, 就会出现这个错误
image.png
正常情况下你可以在数组对象上读取这个 length 属性, 但是如果你要使用的数组对象没有被初始化, 或者因为作用域的问题而没有正确地获取到, 可能就会出现这个错误来看下面这段代码理解下:
- var testArray = ["Test"];
- function testFunction(testArray) {
- for (var i = 0; i < testArray.length; i++) {
- console.log(testArray[i]);
- }
- }
- testFunction();
当你声明函数的参数时, 这些参数就是在函数内部的本地参数这意味着, 你在外部声明的全局变量和本地变量同名了话(都是叫 testArray), 那在函数内部读取的一定是本地的变量, 即传入的参数
有两种方法解决这样的问题
在函数声明时, 去掉这些参数
- 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();
把外部的变量作为参数正确地传给函数内部
- 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, 不能再读取 / 设置它的属性否则, 就会抛出这个错误
image.png
10. ReferenceError: event is not defined
当您尝试访问未定义的变量或当前作用域无法访问到的变量时, 就会出现这个错误
image.png
来源: http://www.jianshu.com/p/01a80b704165