Abstract: Most of the JS error is undefined ...
- Author: Front Ash
- Original: processing the JS undefined 7 Tips
Fundebug authorized reprint, belongs to original author.
About eight years ago, when the author started learning JS, encountered a strange case, there was not only undefined
a value, there is also represent null values null
. Any significant difference between them? They seem to define a null value, and compare null == undefined
the results to true
.
Most modern languages such as Ruby, Python or Java has a null value ( nil
or null
), which seems to be a reasonable way.
For JavaScript, interpreter returned when accessing uninitialized variables or object properties undefined
. E.g:
let company;
company; // => undefined
let person = { name: 'John Smith' };
person.age; // => undefined
On the other hand, null
represents the missing object reference, JS itself is not a variable or object property to null
.
Some native method, for example String.prototype.match()
, may return null
to represent the missing object. Consider the following example:
let array = null;
array; // => null
let movie = { name: "Starship Troopers", musicBy: null };
movie.musicBy; // => null
"abc".match(/[0-9]/); // => null
Due to the characteristics of tolerance JS, developers can easily access uninitialized value, I have made such a mistake.
Typically, this risk will generate undefined
related errors to quickly end the script. Common error messages are related to:
TypeError: 'undefined' is not a function
TypeError: Cannot read property '<prop-name>' of undefined
type errors
JS developers can understand the irony of the joke:
function undefined() {
// problem solved
}
To reduce the risk of such errors, we must understand the generation undefined
situation. More importantly, it appears to inhibit and prevent the spread of the application, thereby improving the durability of the code.
Let us discuss in detail undefined
its impact on the security of the code.
1. undefined What the hell
There are six basic types JS
- Boolean:
true
orfalse
- Number:
1, 6.7, 0xFF
- String:
"Gorilla and banana"
- Symbol:
Symbol("name")
(starting ES2015) - Null:
null
- Undefined:
undefined
.
And a single Object
type: {name: "Dmitri"}, ["apple", "orange"]
.
The ECMAScript specification , from 6 primitive types, undefined
is a special value, it has its own Undefined
type.
The default value is not assigned to a variable
undefined
.
The clearly defined criteria, when uninitialized variable access, there is no object attributes, and the like array element does not exist, a received undefined
value. E.g
let number;
number; // => undefined
let movie = { name: "Interstellar" };
movie.year; // => undefined
let movies = ["Interstellar", "Alexander"];
movies[3]; // => undefined
The code above general process:
- Uninitialized variables
number
- A non-existent object properties
movie.year
- Array element is not present or movies [3]
It will be defined undefined
.
ECMAScript specification defines the undefined
type value
Undefined type is the only value
undefined
type value.
In this sense, typeof undefined
return "undefined" string
typeof undefined === "undefined"; // => true
Of course, typeof
you can verify whether the variable contains a good undefined
value
let nothing;
typeof nothing === "undefined"; // => true
2. The cause of the common scenario undefined
2.1 uninitialized variables
Not yet been assigned (uninitialized) variable declaration of default undefined
.
let myVariable;
myVariable; // => undefined
myVariable
Declared but not yet assigned, the default value undefined
.
Effective way to solve the problem uninitialized variable is assigned an initial value as possible. Uninitialized variables as possible in the state. Ideally, you can declare const myVariable ='Initial value'
specify a value immediately after, but this is not always feasible.
Tip 1: Use const instead of var and let
In my opinion, one of the best features is the use ES6 const
and let
new ways of declaring variables. const
And let
has block scope (with the old function scope var
contrary), in a statement before the line exist in a temporary dead zone .
When the variable-time and permanently receives a value, it is recommended to use const
the statement, it creates an immutable binding.
const
One nice feature is the variable must be const myVariable ='initial'
assigned an initial value. Uninitialized variable is not exposed to the state, and access undefined
is impossible.
The following example checks to verify whether a word is a palindrome functions:
function isPalindrome(word) {
const length = word.length;
const half = Math.floor(length / 2);
for (let index = 0; index < half; index++) {
if (word[index] !== word[length - index - 1]) {
return false;
}
}
return true;
}
isPalindrome("madam"); // => true
isPalindrome("hello"); // => false
length
And half
a variable is assigned once. Declare them as const
seems reasonable, since these variables do not change.
If you need to bind variables (ie, multiple assignment), apply let
statement. Whenever possible, immediately assign an initial value to it, for example let index = 0
.
Then use the var
statement of it, with respect to the ES6, advice is to stop using it entirely.
var
Variables declared mention will be promoted to the top of the entire function scope. Somewhere in a statement at the end of the scope function var
variables, but you can still access it before the statement: the value of the corresponding variable is undefined
.
Instead, use let
or const
can not access the variable before the variable declaration. The reason why this happens is because the variable is declared before the temporary dead zone . This is good because it is such a rare opportunity to access undefined
value.
Using the let
above example (rather than var) update will lead to ReferenceError
error, because they can not access the temporary dead zone variable.
function bigFunction() {
// code...
myVariable; // => Throws 'ReferenceError: myVariable is not defined'
// code...
let myVariable = 'Initial value';
// code...
myVariable; // => 'Initial value'
}
bigFunction();
Tip 2: Increase cohesion within
Describe the degree of the cohesive element (namespace, class, method, code blocks) of the module together. Cohesion measurement is commonly referred to low or high cohesive cohesive.
High cohesion is preferred because it is recommended that the module elements designed to focus only on a single task, it constitutes a module.
- Focused and easy to understand: easier to understand the function module
- Maintainable and easier to reconstruction: change affects fewer modules module
- Reusable: focus on a single task, to make it easier to reuse module
- Testable: easier to focus on a single task test module
High cohesion and low coupling is a well-designed system features.
Code block itself may be considered as a small module, as much as possible in order to achieve high cohesion, it is necessary to use them as close to the variable position of the code block.
For example, if there is only one variable to form a block scope, this variable is not exposed to external block scope as the outer block should not be concerned about this variable.
A typical example is unnecessarily prolonged life cycle is a function of the variables in for
loop used:
function someFunc(array) {
var index, item, length = array.length;
// some code...
// some code...
for (index = 0; index < length; index++) {
item = array[index];
// some code...
}
return 'some result';
}
index
, item
And length
variables declared in the beginning of the function body, but they are only used at the end, in this way then what is the problem?
Statement from the top to the for
statement variable index and the item is uninitialized value undefined
. They have a longer life cycle unreasonable in the entire function scope.
A better approach is to use these variables to move to their positions as much as possible:
function someFunc(array) {
// some code...
// some code...
const length = array.length;
for (let index = 0; index < length; index++) {
const item = array[index];
// some
}
return 'some result';
}
index
And item
variables exist only for
statement of scope, for
than no sense. length
Variable is also declared its location close to its use.
Why modified version better than the original version? The main points:
- Variable unexposed
undefined
state, so there is no access toundefined
risk - The variables as much as possible move to position their use will increase the readability of code
- High cohesive reconstructed code block easier to separate and extract function where necessary
2.2 to access a nonexistent property
When accessing object properties that do not exist, JS returns
undefined
.
Let's use an example to illustrate this point:
let favoriteMovie = {
title: 'Blade Runner'
};
favoriteMovie.actors; // => undefined
favoriteMovie
是一个具有单个属性 title
的对象。 使用属性访问器favoriteMovie.actors
访问不存在的属性actors
将被计算为undefined
。
本身访问不存在的属性不会引发错误, 但尝试从不存在的属性值中获取数据时就会出现问题。 常见的的错误是 TypeError: Cannot read property <prop> of undefined
。
稍微修改前面的代码片段来说明TypeError throw
:
let favoriteMovie = {
title: 'Blade Runner'
};
favoriteMovie.actors[0];
// TypeError: Cannot read property '0' of undefined
favoriteMovie
没有属性actors
,所以favoriteMovie.actors
的值 undefined
。因此,使用表达式favoriteMovie.actors[0]
访问undefined
值的第一项会引发TypeError
。
JS 允许访问不存在的属性,这种允许访问的特性容易引起混淆:可能设置了属性,也可能没有设置属性,绕过这个问题的理想方法是限制对象始终定义它所持有的属性。
不幸的是,咱们常常无法控制对象。在不同的场景中,这些对象可能具有不同的属性集,因此,必须手动处理所有这些场景:
接着我们实现一个函数append(array, toAppend)
,它的主要功能在数组的开头和/或末尾添加新的元素。 toAppend
参数接受具有属性的对象:
- first:元素插入数组的开头
- last:元素在数组末尾插入。
函数返回一个新的数组实例,而不改变原始数组(即它是一个纯函数)。
append()
的第一个版本看起来比较简单,如下所示:
function append(array, toAppend) {
const arrayCopy = array.slice();
if (toAppend.first) {
arrayCopy.unshift(toAppend.first);
}
if (toAppend.last) {
arrayCopy.push(toAppend.last);
}
return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]
append(['Hello'], { last: 'World' }); // => ['Hello', 'World']
append([8, 16], { first: 4 }); // => [4, 8, 16]
由于toAppend
对象可以省略first
或last
属性,因此必须验证toAppend
中是否存在这些属性。如果属性不存在,则属性访问器值为undefined
。
检查first
或last
属性是否是undefined
,在条件为 if(toappendix .first){}
和if(toappendix .last){}
中进行验证:
这种方法有一个缺点, undefined
,false
,null
,0
,NaN
和''
是虚值。
在append()
的当前实现中,该函数不允许插入虚值元素:
append([10], { first: 0, last: false }); // => [10]
0
和false
是虚值的。 因为 if(toAppend.first){}
和if(toAppend.last){}
实际上与falsy
进行比较,所以这些元素不会插入到数组中,该函数返回初始数组[10]
而不会进行任何修改。
以下技巧解释了如何正确检查属性的存在。
技巧3: 检查属性是否存在
JS 提供了许多方法来确定对象是否具有特定属性:
obj.prop!== undefined
:直接与undefined
进行比较typeof obj.prop!=='undefined'
:验证属性值类型obj.hasOwnProperty('prop')
:验证对象是否具有自己的属性'prop' in obj
:验证对象是否具有自己的属性或继承属性
我的建议是使用 in
操作符,它的语法短小精悍。in
操作符的存在表明一个明确的意图,即检查对象是否具有特定的属性,而不访问实际的属性值。
obj.hasOwnProperty('prop')
也是一个很好的解决方案,它比 in
操作符稍长,仅在对象自己的属性中进行验证。
涉及与undefined
进行比较剩下的两种方式可能有效,但在我看来,obj.prop!== undefined
和typeof obj.prop!=='undefined'
看起来冗长而怪异,并暴露出直接处理undefined
的可疑路径。。
让咱们使用in
操作符改进append(array, toAppend)
函数:
function append(array, toAppend) {
const arrayCopy = array.slice();
if ('first' in toAppend) {
arrayCopy.unshift(toAppend.first);
}
if ('last' in toAppend) {
arrayCopy.push(toAppend.last);
}
return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]
append([10], { first: 0, last: false }); // => [0, 10, false]
'first' in toAppend
(和'last' in toAppend
)在对应属性存在时为true
,否则为false
。in
操作符的使用解决了插入虚值元素0
和false
的问题。现在,在[10]
的开头和结尾添加这些元素将产生预期的结果[0,10,false]
。
技巧4:解构访问对象属性
在访问对象属性时,如果属性不存在,有时需要指示默认值。可以使用in
和三元运算符来实现这一点。
const object = { };
const prop = 'prop' in object ? object.prop : 'default';
prop; // => 'default'
当要检查的属性数量增加时,三元运算符语法的使用变得令人生畏。对于每个属性,都必须创建新的代码行来处理默认值,这就增加了一堵难看的墙,里面都是外观相似的三元运算符。
为了使用更优雅的方法,可以使用 ES6 对象的解构。
对象解构允许将对象属性值直接提取到变量中,并在属性不存在时设置默认值,避免直接处理undefined
的方便语法。
实际上,属性提取现在看起来简短而有意义:
const object = { };
const { prop = 'default' } = object;
prop; // => 'default'
要查看实际操作中的内容,让我们定义一个将字符串包装在引号中的有用函数。quote(subject, config)
接受第一个参数作为要包装的字符串。 第二个参数config
是一个具有以下属性的对象:
- char:包装的字符,例如
'
(单引号)或“
(双引号),默认为”
。 skipIfQuoted
:如果字符串已被引用则跳过引用的布尔值,默认为true
。
使用对象析构的优点,让咱们实现quote()
function quote(str, config) {
const { char = '"', skipIfQuoted = true } = config;
const length = str.length;
if (skipIfQuoted
&& str[0] === char
&& str[length - 1] === char) {
return str;
}
return char + str + char;
}
quote('Hello World', { char: '*' }); // => '*Hello World*'
quote('"Welcome"', { skipIfQuoted: true }); // => '"Welcome"'
const {char = '", skipifquote = true} = config
解构赋值在一行中从config
对象中提取char
和skipifquote
属性。如果config
对象中有一些属性不可用,那么解构赋值将设置默认值:char
为'"'
,skipifquote
为false
。
该功能仍有改进的空间。让我们将解构赋值直接移动到参数部分。并为config
参数设置一个默认值(空对象{}
),以便在默认设置足够时跳过第二个参数。
function quote(str, { char = '"', skipIfQuoted = true } = {}) {
const length = str.length;
if (skipIfQuoted
&& str[0] === char
&& str[length - 1] === char) {
return str;
}
return char + str + char;
}
quote('Hello World', { char: '*' }); // => '*Hello World*'
quote('Sunny day'); // => '"Sunny day"'
注意,解构赋值替换了函数 config
参数。我喜欢这样:quote()
缩短了一行。
={}
在解构赋值的右侧,确保在完全没有指定第二个参数的情况下使用空对象。
对象解构是一个强大的功能,可以有效地处理从对象中提取属性。 我喜欢在被访问属性不存在时指定要返回的默认值的可能性。因为这样可以避免undefined
以及与处理它相关的问题。
技巧5: 用默认属性填充对象
如果不需要像解构赋值那样为每个属性创建变量,那么丢失某些属性的对象可以用默认值填充。
ES6 Object.assign(target,source1,source2,...)
将所有可枚举的自有属性的值从一个或多个源对象复制到目标对象中,该函数返回目标对象。
例如,需要访问unsafeOptions
对象的属性,该对象并不总是包含其完整的属性集。
为了避免从unsafeOptions
访问不存在的属性,让我们做一些调整:
- 定义包含默认属性值的
defaults
对象 - 调用
Object.assign({},defaults,unsafeOptions)
来构建新的对象options
。 新对象从unsafeOptions
接收所有属性,但缺少的属性从defaults
对象获取。
const unsafeOptions = {
fontSize: 18
};
const defaults = {
fontSize: 16,
color: 'black'
};
const options = Object.assign({}, defaults, unsafeOptions);
options.fontSize; // => 18
options.color; // => 'black'
unsafeOptions
仅包含fontSize
属性。 defaults
对象定义属性fontSize
和color
的默认值。
Object.assign()
将第一个参数作为目标对象{}
。 目标对象从unsafeOptions
源对象接收fontSize
属性的值。 并且人defaults
对象的获取color
属性值,因为unsafeOptions
不包含color
属性。
枚举源对象的顺序很重要:后面的源对象属性会覆盖前面的源对象属性。
现在可以安全地访问options
对象的任何属性,包括options.color
在最初的unsafeOptions
中是不可用的。
还有一种简单的方法就是使用ES6中展开运算符:
const unsafeOptions = {
fontSize: 18
};
const defaults = {
fontSize: 16,
color: 'black'
};
const options = {
...defaults,
...unsafeOptions
};
options.fontSize; // => 18
options.color; // => 'black'
对象初始值设定项从defaults
和unsafeOptions
源对象扩展属性。 指定源对象的顺序很重要,后面的源对象属性会覆盖前面的源对象。
使用默认属性值填充不完整的对象是使代码安全且持久的有效策略。无论哪种情况,对象总是包含完整的属性集:并且无法生成undefined
的属性。
2.3 函数参数
函数参数隐式默认为
undefined
。
通常,用特定数量的参数定义的函数应该用相同数量的参数调用。在这种情况下,参数得到期望的值
function multiply(a, b) {
a; // => 5
b; // => 3
return a * b;
}
multiply(5, 3); // => 15
调用multiply(5,3)
使参数a
和b
接收相应的5
和3
值,返回结果:5 * 3 = 15
。
在调用时省略参数会发生什么?
function multiply(a, b) {
a; // => 5
b; // => undefined
return a * b;
}
multiply(5); // => NaN
函数multiply(a, b){}
由两个参数a
和b
定义。调用multiply(5)
用一个参数执行:结果一个参数是5
,但是b
参数是undefined
。
技巧6: 使用默认参数值
有时函数不需要调用的完整参数集,可以简单地为没有值的参数设置默认值。
回顾前面的例子,让我们做一个改进,如果b
参数未定义,则为其分配默认值2
:
function multiply(a, b) {
if (b === undefined) {
b = 2;
}
a; // => 5
b; // => 2
return a * b;
}
multiply(5); // => 10
虽然所提供的分配默认值的方法有效,但不建议直接与undefined
值进行比较。它很冗长,看起来像一个hack .
这里可以使用 ES6 的默认值:
function multiply(a, b = 2) {
a; // => 5
b; // => 2
return a * b;
}
multiply(5); // => 10
multiply(5, undefined); // => 10
2.4 函数返回值
隐式地,没有
return
语句,JS 函数返回undefined
。
在JS中,没有任何return
语句的函数隐式返回undefined
:
function square(x) {
const res = x * x;
}
square(2); // => undefined
square()
函数没有返回计算结果,函数调用时的结果undefined
。
当return
语句后面没有表达式时,默认返回 undefined
。
function square(x) {
const res = x * x;
return;
}
square(2); // => undefined
return;
语句被执行,但它不返回任何表达式,调用结果也是undefined
。
function square(x) {
const res = x * x;
return res;
}
square(2); // => 4
技巧7: 不要相信自动插入分号
JS 中的以下语句列表必须以分号(;)
结尾:
- 空语句
let,const,var,import,export
声明- 表达语句
debugger
语句continue
语句,break
语句throw
语句return
语句
如果使用上述声明之一,请尽量务必在结尾处指明分号:
function getNum() {
let num = 1;
return num;
}
getNum(); // => 1
let
声明和return
语句结束时,强制性写分号。
当你不想写这些分号时会发生什么? 例如,咱们想要减小源文件的大小。
在这种情况下,ECMAScript 提供自动分号插入(ASI)机制,为你插入缺少的分号。
ASI 的帮助下,可以从上一个示例中删除分号:
function getNum() {
// Notice that semicolons are missing
let num = 1
return num
}
getNum() // => 1
上面的代码是有效的JS代码,缺少的分号ASI会自动为我们插入。
乍一看,它看起来很 nice。 ASI 机制允许你少写不必要的分号,可以使JS代码更小,更易于阅读。
ASI 创建了一个小而烦人的陷阱。 当换行符位于return
和return \n expression
之间时,ASI 会在换行符之前自动插入分号(return; \n expression
)。
函数内部return;
? 即该函数返回undefined
。 如果你不详细了解ASI的机制,则意外返回的undefined
会产生意想不到的问题。
来 getPrimeNumbers()
调用返回的值:
function getPrimeNumbers() {
return
[ 2, 3, 5, 7, 11, 13, 17 ]
}
getPrimeNumbers() // => undefined
在return
语句和数组之间存在一个换行,JS 在return
后自动插入分号,解释代码如下:
function getPrimeNumbers() {
return;
[ 2, 3, 5, 7, 11, 13, 17 ];
}
getPrimeNumbers(); // => undefined
return;
使函数getPrimeNumbers()
返回undefined
而不是期望的数组。
这个问题通过删除return
和数组文字之间的换行来解决:
function getPrimeNumbers() {
return [
2, 3, 5, 7, 11, 13, 17
];
}
getPrimeNumbers(); // => [2, 3, 5, 7, 11, 13, 17]
我的建议是研究自动分号插入的确切方式,以避免这种情况。
当然,永远不要在return
和返回的表达式之间放置换行符。
2.5 void 操作符
void <expression>
计算表达式无论计算结果如何都返回undefined
。
void 1; // => undefined
void (false); // => undefined
void {name: 'John Smith'}; // => undefined
void Math.min(1, 3); // => undefined
void
操作符的一个用例是将表达式求值限制为undefined
,这依赖于求值的一些副作用。
3. 未定义的数组
访问越界索引的数组元素时,会得到undefined
。
const colors = ['blue', 'white', 'red'];
colors[5]; // => undefined
colors[-1]; // => undefined
colors
数组有3个元素,因此有效索引为0,1
和2
。
因为索引5
和-1
没有数组元素,所以访问colors[5]
和colors[-1]
值为undefined
。
JS 中,可能会遇到所谓的稀疏数组。这些数组是有间隙的数组,也就是说,在某些索引中,没有定义元素。
当在稀疏数组中访问间隙(也称为空槽)时,也会得到一个undefined
。
下面的示例生成稀疏数组并尝试访问它们的空槽
const sparse1 = new Array(3);
sparse1; // => [<empty slot>, <empty slot>, <empty slot>]
sparse1[0]; // => undefined
sparse1[1]; // => undefined
const sparse2 = ['white', ,'blue']
sparse2; // => ['white', <empty slot>, 'blue']
sparse2[1]; // => undefined
使用数组时,为了避免获取undefined
,请确保使用有效的数组索引并避免创建稀疏数组。
4. undefined和null之间的区别
一个合理的问题出现了:undefined
和null
之间的主要区别是什么?这两个特殊值都表示为空状态。
主要区别在于undefined
表示尚未初始化的变量的值,null
表示故意不存在对象。
让咱们通过一些例子来探讨它们之间的区别。
number 定义了但没有赋值。
let number;
number; // => undefined
number
变量未定义,这清楚地表明未初始化的变量。
当访问不存在的对象属性时,也会发生相同的未初始化概念
const obj = { firstName: 'Dmitri' };
obj.lastName; // => undefined
因为obj
中不存在lastName
属性,所以JS正确地将obj.lastName
计算为undefined
。
在其他情况下,你知道变量期望保存一个对象或一个函数来返回一个对象。但是由于某些原因,你不能实例化该对象。在这种情况下,null
是丢失对象的有意义的指示器。
例如,clone()
是一个克隆普通JS对象的函数,函数将返回一个对象
function clone(obj) {
if (typeof obj === 'object' && obj !== null) {
return Object.assign({}, obj);
}
return null;
}
clone({name: 'John'}); // => {name: 'John'}
clone(15); // => null
clone(null); // => null
但是,可以使用非对象参数调用clone()
: 15
或null
(或者通常是一个原始值,null
或undefined
)。在这种情况下,函数不能创建克隆,因此返回null
—— 一个缺失对象的指示符。
typeof
操作符区分了这两个值
typeof undefined; // => 'undefined'
typeof null; // => 'object'
严格相等运算符===
可以正确区分undefined
和null
:
let nothing = undefined;
let missingObject = null;
nothing === missingObject; // => false
总结
undefined
的存在是JS的允许性质的结果,它允许使用:
- 未初始化的变量
- 不存在的对象属性或方法
- 访问越界索引的数组元素
- 不返回任何结果的函数的调用结果
大多数情况下直接与undefined
进行比较是一种不好的做法。一个有效的策略是减少代码中undefined
关键字的出现:
- 减少未初始化变量的使用
- 使变量生命周期变短并接近其使用的位置
- 尽可能为变量分配初始值
- 多敷衍 const 和 let
- 使用默认值来表示无关紧要的函数参数
- 验证属性是否存在或使用默认属性填充不安全对象
- 避免使用稀疏数组
关于Fundebug
Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用!