JavaScript 新手的踩坑日记

引语

在1995年5月,Eich 大神在10天内就写出了第一个脚本语言的版本,JavaScript 的第一个代号是 Mocha,Marc Andreesen 起的这个名字。因为商标问题以及不少产品已经使用了 Live 的前缀,网景市场部将它更名为 LiveScript。在1995年11月底,Navigator 2.0B3 发行,其中包含了该语言的原型,这个版本相比以前没有什么大的变化。在1995年12月初,Java 语言发展壮大,Sun 把 Java 的商标受权给了网景。这个语言被再次更名,变成了最终的名字——JavaScript。在以后的1997年1月,标准化之后,就成为如今的 ECMAScript。javascript

近一两年在客户端上用到 JS 的地方也愈来愈多了,笔者最近接触了一下 JS ,做为前端小白,记录一下近期本身“踩坑”的成长经历。前端

一. 原始值和对象

在 JavaScript 中,对值的区分就两种:java

1.原始值:BOOL,Number,String,null,undefined。
2.对象:每一个对象都有惟一的标识且只严格的等于(===)本身。git

null,undefined没有属性,连toString( )方法也没有。github

false,0,NaN,undefined,null,' ' ,都是false。编程

typeof 运算符能区分原始值和对象,并检测出原始值的类型。
instanceof 运算符能够检测出一个对象是不是特定构造函数的一个实例或者是否为它的一个子类。数组

操做数 typeof
undefined 'undefined'
null object
布尔值 boolean
数字 number
字符串 string
函数 function
其余的常规值 object
引擎建立的值 可能返回任意的字符串

null 返回的是一个 object,这个是一个不可修复的 bug,若是修改这个 bug,就会破坏现有代码体系。可是这不能表示 null 是一个对象。安全

由于第一代 JavaScript 引擎中的 JavaScript 值表示为32位的字符。最低3位做为一种标识,表示值是对象,整数,浮点数或者布尔值。对象的标识是000,而为了表现 null ,引擎使用了机器语言 NULL 的指针,该字符的全部位都是0。而 typeof 就是检测值的标志位,这就是为何它会认为 null 是一个对象了。数据结构

因此判断 一个 value 是否是一个对象应该按照以下条件判断:app

function isObject (value) {
  return ( value !== null 
    && (typeof value === 'object' 
    || typeof value === 'function'));
}复制代码

null 是原型链最顶端的元素

Object.getPrototypeOf(Object.prototype)

< null复制代码

判断 undefined 和 null 能够用严格相等判断:

if(x === null) {
  // 判断是否为 null
}

if (x === undefined) {
  // 判断是否为 undefined
}

if (x === void 0 ) {
  // 判断是否为 undefined,void 0 === undefined
}

if (x != null ) {
 // 判断x既不是undefined,也不是null
 // 这种写法等价于 if (x !== undefined && x !== null )
}复制代码

在原始值里面有一个特例,NaN 虽然是原始值,可是它和它自己是不相等的。

NaN === NaN
<false复制代码

原始值的构造函数 Boolean,Number,String 能够把原始值转换成对象,也能够把对象转换成原始值。

// 原始值转换成对象
var object = new String('abc')


// 对象转换成原始值
String(123)
<'123'复制代码

可是在对象转换成原始值的时候,须要注意一点:若是用 valueOf() 函数进行转换的时候,转换一切正确。

new Boolean(true).valueOf()
<true复制代码

可是使用构造函数将包装对象转换成原始值的时候,BOOL值是不能正确被转换的。

Boolean(new Boolean(false))
<true复制代码

构造函数只能正确的提取出包装对象中的数字和字符串。

二. 宽松相等带来的bug

在 JavaScript 中有两种方式来判断两个值是否相等。

  1. 严格相等 ( === ) 和严格不等 ( !== ) 要求比较的值必须是相同的类型。
  2. 宽松相等 ( == ) 和宽松不等 ( != ) 会先尝试将两个不一样类型的值进行转换,而后再使用严格等进行比较。

宽松相等就会遇到一些bug:

undefined == null // undefined 和 null 是宽松相等的
<true

2 == true  // 不要误认为这里是true
<false

1 == true 
<true

0 == false
<true 

' ' == false // 空字符串等于false,可是不是全部的非空字符串都等于true
<true

'1' == true
<true

'2' == true
<false

'abc' == true // NaN === 1
<false复制代码

关于严格相等( Strict equality ) 和 宽松相等( Loose equality ),GitHub上有一我的总结了一张图,挺好的,贴出来分享一下,Github地址在这里

可是若是用 Boolean( ) 进行转换的时候状况又有不一样:

转换成BOOL值
undefined false
null false
BOOL 与输入值相同
数字 0,NaN 转换成false,其余的都为 true
字符串 ' '转换成false,其余字符串都转换成true
对象 全为true

这里为什么对象老是为true ?
在 ECMAScript 1中,曾经规定不支持经过对象配置来转换(好比 toBoolean() 方法)。原理是布尔运算符 || 和 && 会保持运算数的值。所以,若是链式使用这些运算符,会屡次确认相同值的真假。这样的检查对于原始值类型成本不大,可是对于对象,若是能经过配置来转换布尔值,成本很大。因此从 ECMAScript 1 开始,对象老是为 true 来避免了这些成本转换。

三. Number

JavaScript 中全部的数字都只有一种类型,都被当作浮点数,JavaScript 内部会作优化,来区分浮点数组和整数。JavaScript 的数字是双精度的(64位),基于 IEEE 754 标准。

因为全部数字都是浮点数,因此这里就会有精度的问题。还记得前段时间网上流传的机器人的漫画么?

精度的问题就会引起一些奇妙的事情

0.1 + 0.2 ;  // 0.300000000000004

( 0.1 + 0.2 ) + 0.3;    // 0.6000000000001
0.1 + ( 0.2 + 0.3 );    // 0.6

(0.8+0.7+0.6+0.5) / 4   // 0.65
(0.6+0.7+0.8+0.5) / 4   // 0.6499999999999999复制代码

变换一个位置,加一个括号,都会影响精度。为了不这个问题,建议仍是转换成整数。

( 8 + 7 + 6 + 5) / 4 / 10 ;  // 0.65
( 6 + 8 + 5 + 7) / 4 / 10 ;  // 0.65复制代码
转换成Number值
undefined NaN
null 0
BOOL false = 0,true = 1
数字 与原值相同
字符串 解析字符串中的数字(忽略开头和结尾的空格);空字符转换成0。
对象 调用 ToPrimitive( value,number) 并转换成原始类型

在数字里面有4个特殊的数值:

  1. 2个错误值:NaN 和 Infinity
  2. 2个0,一个+0,一个-0。0是会带正号和负号。由于正负号和数值是分开存储的。
typeof NaN
<"number"复制代码

(吐槽:NaN 是 “ not a number ”的缩写,可是它倒是一个数字)

NaN 是 JS 中惟一一个不能自身严格相等的值:

NaN === NaN
<false复制代码

因此不能经过 Array.prototype.indexOf 方法去查找 NaN (由于数组的 indexOf 方法会进行严格等的判断)。

[ NaN ].indexOf( NaN )
<-1复制代码

正确的姿式有两种:

第一种:

function realIsNaN( value ){
  return typeof value === 'number' && isNaN(value);
}复制代码

上面这种之因此须要判断类型,是由于字符串转换会先转换成数字,转换失败为 NaN。因此和 NaN 相等。

isNaN( 'halfrost' )
<true复制代码

第二种方法是利用 IEEE 754 标准里面的定义,NaN 和任意值比较,包括和自身进行比较,都是无序的

function realIsNaN( value ){
  return value !== value ;
}复制代码

另一个错误值 Infinity 是由表示无穷大,或者除以0致使的。

判断它直接用 宽松相等 == ,或者严格相等 === 判断便可。

可是 isFinite() 函数不是专门用来判断Infinity的,是用来判断一个值是不是错误值(这里表示既不是 NaN,又不是 Infinity,排除掉这两个错误值)。

在 ES6 中 引入了两个函数专门判断 Infinity 和 NaN的,Number.isFinite() 和 Number.isNaN() 之后都建议用这两个函数进行判断。

JS 中整型是有一个安全区间,在( -2^53 , 2^53)之间。因此若是数字超过了64位无符号的整型数字,就只能用字符串进行存储了。

利用 parseInt() 进行转换成数字的时候,会有出错的时候,结果不可信:

parseInt(1000000000000000000000000000.99999999999999999,10)
<1复制代码

parseInt( str , redix? ) 会先把第一个参数转换成字符串:

String(1000000000000000000000000000.99999999999999999)
<"1e+27"复制代码

parseInt 不认为 e 是整数,因此在 e 以后的就中止解析了,因此最终输出1。

JS 中的 % 求余操做符并非咱们平时认为的取模。

-9%7
<-2复制代码

求余操做符会返回一个和第一个操做数相同符号的结果。取模运算是和第二个操做数符号相同。

因此比较坑的就是咱们平时判断一个数是不是奇偶数的问题就会出现错误:

function isOdd( value ){
  return value % 2 === 1;
}

console.log(-3);  // false
console.log(-2);  // false复制代码

正确姿式是:

function isOdd( value ){
  return Math.abs( value % 2 ) === 1;
}

console.log(-3);  // true
console.log(-2);  // false复制代码

四. String

字符串比较符,是没法比较变音符和重音符的。

'ä' < 'b'
<false

'á' < 'b'
<false复制代码

五. Array

建立数组的时候不能用单个数字建立数组。

new Array(2)  // 这里的一个数字表明的是数组的长度
<[ , , ]

new Array(2,3,4)
<[2,3,4]复制代码

删除元素会删出空格,可是不会改变数组的长度。

var array = [1,2,3,4]
array.length
<4
delete array[1]

array
<[1, ,3,4]
array.length
<4复制代码

因此这里的删除不是很符合咱们以前的删除,正确姿式是用splice

var array = [1,2,3,4,56,7,8,9]
array.splice(1,3)
array
<[1, 56, 7, 8, 9]
array.length
<5复制代码

针对数组里面的空缺,不一样的遍历方法行为不一样

在 ES5 中:

方法 针对空缺
forEach() 遍历时跳过空缺
every() 遍历时跳过空缺
some() 遍历时跳过空缺
map() 遍历时跳过空缺,可是最终结果会保留空缺
filter() 去除空缺
join() 把空缺,undefined,null转化为空字符串
toString() 把空缺,undefined,null转化为空字符串
sort() 排序时保留空缺
apply() 把每一个空缺转化为undefined

在 ES6 中:规定,遍历时不跳过空缺,空缺都转化为undefined

方法 针对空缺
Array.from() 空缺都转化为undefined
...(扩展运算符有) 空缺都转化为undefined
copyWithin() 连空缺一块儿复制
fill() 遍历时不跳过空缺,视空缺为正常的元素
for...of 遍历时不跳过空缺
entries() 空缺都转化为undefined
keys() 空缺都转化为undefined
values() 空缺都转化为undefined
find() 空缺都转化为undefined
findIndex() 空缺都转化为undefined p0p0

六. Set 、Map、WeakSet、WeakMap

数据结构 特色
Set 相似于数组,可是成员值惟一,注意(这里是一个例外),这里 NaN 等于自身
WeakSet 成员只能是对象,而不能是其余类型的值。对象的引用都是弱引用,因此不能引用 WeakSet 的成员,不可遍历它(由于遍历的过程当中随时均可以消失)
Map 相似于对象,键值对的集合,键的范围不限于字符串,各类类型均可以,是“值—值”的映射,这一点区别于对象的“字符串—值”的映射
WeakMap 于 Map 相似,区别在于它只接受对象做为键名( null 除外),键名指向的对象也不计入垃圾回收机制中,它也没法遍历,也没法清空clear

七. 循环

先说一个 for-in 的坑:

var scores = [ 11,22,33,44,55,66,77 ];
var total = 0;
for (var score in scores) {
  total += score;
}

var mean = total / scores.length;

mean;复制代码

通常人看到这道题确定就开始算了,累加,而后除以7 。那么这题就错了,若是把数组里面的元素变的更加复杂:

var scores = [ 1242351,252352,32143,452354,51455,66125,74217 ];复制代码

其实这里答案和数组里面元素是多少无关。只要数组元素个数是7,最终答案都是17636.571428571428。

缘由是 for-in 循环的是数组下标,因此 total = ‘00123456’ ,而后这个字符串再除以7。

循环方式 遍历对象 反作用
for 写法比较麻烦
for-in 索引值(键名),而非数组元素 遍历全部(非索引)属性,以及继承过来的属性(能够用hasOwnProperty()方法排除继承属性),主要是为遍历对象而设计的,不适用于遍历数组
forEach 不方便break,continue,return
for...of 内部经过调用 Symbol.iterator 方法,实现遍历得到键值 不可遍历普通的对象,由于没有 Iterator 接口

遍历对象的属性,ES6 中有6种方法:

循环方式 遍历对象
for...in 循环遍历对象自身的和继承的可枚举属性(不包含Symbol属性))
Object.key(obj) 返回一个数组,包括对象自身的(不含继承的)全部可枚举属性(不含Symbol属性)
Object.getOwnPropertyNames(obj) 返回一个数组,包含对象自身的全部属性(不含 Symbol 属性,可是包含不可枚举的属性)
Object.getOwnPropertySymbols(obj) 返回一个数组,包含对象自身的全部 Symbol 属性
Reflect.ownKeys(obj) 返回一个数组,包含对象自身的全部属性,无论属性名是 Symbol 或者字符串或者是否可枚举
Reflect.enumerate(obj) 返回一个 Iterator对象,遍历对象自身的和继承的全部可枚举属性(不包含 Symbol 属性),与 for...in循环相同

八. 隐式转换 / 强制转换 带来的bug

var formData = { width : '100'};

var w = formData.width;
var outer = w + 20;

console.log( outer === 120 ); // false;
console.log( outer === '10020'); // true复制代码

九. 运算符重载

在 JavaScript 没法重载或者自定义运算符,包括等号。

十. 函数声明和变量声明的提高

先举一个函数提高的例子。

function foo() {
  bar();
  function bar() {
    ……
  }
}复制代码

var 变量也具备提高的特性。可是把函数赋值给变量之后,提高的效果就会消失。

function foo() {
  bar(); // error!
  var bar = function () {
    ……
  }
}复制代码

上述函数就没有提高效果了。

函数声明是作了彻底提高,变量声明只是作了部分提高。变量的声明才有提高的做用,赋值的过程并不会提高。

JavaScript 支持词法做用域( lexical scoping ),即除了极少的例外,对变量 foo 的引用会被绑定到声明 foo 变量最近的做用域中。ES5中 不支持块级做用域,即变量定义的做用域并非离其最近的封闭语句或代码块,而包含它们的函数。全部的变量声明都会被提高,声明会被移动到函数的开始处,而赋值则仍然会在原来的位置进行。

function foo() {
  var x = -10;
  if ( x < 0) {
    var tmp = -x;
    ……
 }
 console.log(tmp);  // 10
}复制代码

这里 tmp 就有变量提高的效果。

再举个例子:

foo = 2;
var foo; 
console.log( foo );复制代码

上面这个例子仍是输出2,不是输出undefined。

这个通过编译器编译之后,其实会变成下面这个样子:

var foo; 
foo = 2;
console.log( foo );复制代码

变量声明被提早了,赋值还在原地。 为了加深一下这句话的理解,再举一个例子:

console.log( a ); 
var a = 2;复制代码

上述代码会被编译成下面的样子:

var foo;
console.log( foo ); 
foo = 2;复制代码

因此输出的是undefined。

若是变量和函数都存在提高的状况,那么函数提高优先级更高

foo(); // 1
var foo;
function foo() { 
    console.log( 1 );
}
foo = function() { 
    console.log( 2 );
};复制代码

上面通过编译过会变成下面这样子:

function foo() { 
   console.log( 1 );
}
foo(); // 1
foo = function() { 
   console.log( 2 );
};复制代码

最终结果输出是1,不是2 。这就说明了函数提高是优先于变量提高的。

为了不变量提高,ES6中引入了 let 和 const 关键字,使用这两个关键字就不会有变量提高了。原理是,在代码块内,使用 let 命令声明变量以前,该变量都是不可用的,这块区域叫“暂时性死区”(temporal dead zone,TDZ)。TDZ 的作法是,只要一进入到这一区域,所要使用的变量就已经存在了,变量仍是“提高”了,可是不能获取,只有等到声明变量的那一行出现,才能够获取和使用该变量。

ES6 的这种作法也给 JS 带来了块级做用域,(在 ES5 中只有全局做用于和函数做用域),因而当即执行匿名函数(IIFE)就不在必要了。

十一. arguments 不是数组

arguments 不是数组,它只是相似于数组。它有length属性,能够经过方括号去访问它的元素。不能移除它的元素,也不能对它调用数组的方法。

不要在函数体内使用 arguments 变量,使用 rest 运算符( ... )代替。由于 rest 运算符显式代表了你想要获取的参数,并且 arguments 仅仅只是一个相似的数组,而 rest 运算符提供的是一个真正的数组。

下面有一个把 arguments 当数组用的例子:

function callMethod(obj,method) {
  var shift = [].shift;
  shift.call(arguments);
  shift.call(arguments);
  return obj[method].apply(obj,arguments);
}

var obj = {
  add:function(x,y) { return x + y ;}
};

callMethod(obj,"add",18,38);复制代码

上述代码直接报错:

Uncaught TypeError: Cannot read property 'apply' of undefined
    at callMethod (<anonymous>:5:21)
    at <anonymous>:12:1复制代码

出错的缘由就在于 arguments 并非函数参数的副本,全部命名参数都是 arguments 对象中对应索引的别名。所以经过 shift 方法移除 arguments 对象中的元素以后,obj 仍然是 arguments[0] 的别名,method 仍然是 arguments[1] 的别名。看上去是在调用 obj[add],其实是在调用17[25]。

还有一个问题,使用 arguments 引用的时候。

function values() {
  var i = 0 , n = arguments.length;
  return {
      hasNext: function() {
        return i < n;
      },
      next: function() {
        if (i >= n) {
            throw new Error("end of iteration");
        }
        return arguments[i++];
      }
  }
}

var it = values(1,24,53,253,26,326,);
it.next();   // undefined
it.next();   // undefined
it.next();   // undefined复制代码

上述代码是想构造一个迭代器来遍历 arguments 对象的元素。这里之因此会输出 undefined,是由于有一个新的 arguments 变量被隐式的绑定到了每一个函数体内,每一个迭代器 next 方法含有本身的 arguments 变量,因此执行 it.next 的参数时,已经不是 values 函数中的参数了。

更改方式也简单,只要声明一个局部变量,next 的时候能引用到这个变量便可。

function values() {
  var i = 0 , n = arguments.length,a = arguments;
  return {
      hasNext: function() {
        return i < n;
      },
      next: function() {
        if (i >= n) {
            throw new Error("end of iteration");
        }
        return a[i++];
      }
  }
}

var it = values(1,24,53,253,26,326,);
it.next();   // 1
it.next();   // 24
it.next();   // 53复制代码

十二. IIFE 引入新的做用域

在 ES5 中 IIFE 是为了解决 JS 缺乏块级做用域,可是到了 ES6 中,这个就能够不须要了。

十三. 函数中 this 的问题

在嵌套函数中不能访问方法中的 this 变量。

var halfrost = {
    name:'halfrost',
    friends: [ 'haha' , 'hehe' ],
    sayHiToFriends: function() {
 'use strict';
      this.friends.forEach(function (friend) {
          // 'this' is undefined here
          console.log(this.name + 'say hi to' + friend);
      });
    }
}

halfrost.sayHiToFriends()复制代码

这时就会出现一个TypeError: Cannot read property 'name' of undefined。

解决这个问题有两种方法:

第一种:将 this 保存在变量中。

sayHiToFriends: function() {
 'use strict';
  var that = this;
  this.friends.forEach(function (friend) {
      console.log(that.name + 'say hi to' + friend);
  });
}复制代码

第二种:利用bind()函数

使用bind()给回调函数的this绑定固定值,即函数的this

sayHiToFriends: function() {
 'use strict';
  this.friends.forEach(function (friend) {
      console.log(this.name + 'say hi to' + friend);
  }.bind(this));
}复制代码

第三种:利用 forEach 的第二个参数,把 this 指定一个值。

sayHiToFriends: function() {
 'use strict';
  this.friends.forEach(function (friend) {
      console.log(this.name + 'say hi to' + friend);
  }, this);
}复制代码

到了 ES6 里面,建议能用箭头函数的地方用箭头函数。

简单的,单行的,不会复用的函数,都建议用箭头函数,若是函数体很复杂,行数不少,还应该用传统写法。

箭头函数里面的 this 对象就是定义时候的对象,而不是使用时候的对象,这里存在“绑定关系”。

这里的“绑定”机制并非箭头函数带来的,而是由于箭头函数根本就没有本身的 this,致使内部的 this 就是外层代码块的 this,正由于这个特性,也致使了如下的状况都不能使用箭头函数:

  1. 不能当作构造函数,不能使用 new 命令,由于没有 this,不然会抛出一个错误。
  2. 不可使用 argument 对象,该对象在函数体内不存在,非要使用就只能用 rest 参数代替。也不能使用 super,new.target 。
  3. 不可使用 yield 命令,不能做为 Generator 函数。
  4. 不可使用call(),apply(),bind()这些方法改变 this 的指向。

十四. 异步

异步编程有如下几种:

  1. 回调函数callback
  2. 事件监听
  3. 发布 / 订阅
  4. Promise对象
  5. Async / Await

(这个日记可能一直未完待续......)

相关文章
相关标签/搜索