50 tips of JavaScript,这些坑你都知道吗?
1、在局部作用域中,使用var操作符定义的变量将成为定义该变量的作用域中的局部变量,省略var的会创建全局变量;在全局作用域中,不管是否使用var操作符定义的变量都会创建一个全局变量。但是,在全局作用域中使用var创建的全局变量是不能被delete删除的,而未使用var创建的变量和局部作用域中未使用var操作符创建的全局变量是可以删除的。(与其说省略var会创建全局变量,倒不如说省略var会直接给全局对象添加一个新的属性,因为ES中的变量只能通过var关键字才能创建);
var a = 1; // 全局作用域使用var创建全局变量 b = 2; // 全局作用域未使用var创建全局变量 function fn() { c = 3; // 局部作用域未使用var,创建全局变量 } fn(); console.log(a); //1 console.log(b); //2 console.log(c); //3 delete a; //false delete b; //true delete c; //true console.log(typeof a); //number console.log(typeof b); //undefined console.log(typeof c); //undefined
2、浏览器中,Window对象作为全局对象,全局作用域中声明的变量、函数都会变成window对象的属性和方法。然而,定义全局变量和在window对象上直接定义属性是有差别的: 全局变量不能通过delete操作符删除,而直接定义在window对象上的属性可以。原因是通过var语句添加的window属性的[[Configurable]]特性被设置为false。 例如:
var a = 10; window.b = 20; //IE < 9 时抛出错误,其他浏览器中返回false delete window.a; //IE < 9 时抛出错误,其他浏览器中返回true delete window.b; alert(window.a); //10 alert(window.b); //undefined
3、undefined不是关键字,也不是保留字。在某些低版本的浏览器(例如IE8、IE7)中值是可以被修改的(在ECMAScript3中,undefined是可读/写的变量,可以给它赋任意值,这个错误在ECMAScript5中做了修正),在其他浏览器中是可以被赋值的,但是值不会改变。另外eval、arguments之类的也不是关键字或者保留字,并且可以更改它们的值。
undefined = 1; console.log(undefined); //undefined //IE8中 undefined = 1; console.log(undefined); //1 eval = 1; console.log(eval); //1
4、如果在文档开始没发现文档声明,则浏览器会默认开启混杂模式。
5、typeof null 的值为"object",含义:null值表示空对象指针。然而:
typeof null //"object" null instanceof Object //false
6、Array类型的原型是数组
Array.isArray(Array.prototype ); //true Array.isArray(String.prototype ) //false
7、浮点数中必须包含一个小数点,并且小数点后面必须至少有一位数字,如果小数点后面没有任何数字,那么这个数字会作为整数值保存;同样,如果这个数小数点后面只有0(本身就是一个整数),那么它也会作为整数值保存。这么做的原因:保存浮点数值需要的内存空间是保存整数值的两倍,为了节省内存。
1..toString().length //1 (1.+"").length //1 1.0.toString().length //1 (1.0+"").length //1 (1.0000000000+"").length //1 1.0000000000.toString().length //1 (1.00000000001+"").length //13 1.00000000001.toString().length //13
8、如果整数后面跟着一个句点(.),那么JavaScript引擎会将它理解为前面整数的小数点
1.toString() //SyntaxError: identifier starts immediately after numeric literal JavaScript引擎希望看到的数字而不是toString() (1).toString() // "1" 1..toString() // "1" 1...toString() //SyntaxError: missing name after . operator
9、JavaScript中浮点数值的最高精度为17位小数,浮点数值计算会产生舍入误差。所以尽量避免测试某个特定的浮点数的值。
0.1 + 0.2 == 0.3 //false 0.1 + 0.2 == 0.30000000000000004 //true 0.8 - 0.2 //0.6000000000000001 0.8 - 0.6 //0.20000000000000007
10、NaN表示"不是数字类型",但是typeof NaN的值为"number"。NaN不等于任何值,包括它自己。
typeof NaN //"number" NaN == NaN //false
11、对未初始化和未声明的变量执行typeof操作符都会返回undefined值。但是未初始化的变量我们可以进行加减乘除等操作,而未声明的操作只能进行typeof操作,除此之外都会报错。
b + "" // Uncaught ReferenceError: b is not defined typeof b //"undefined" var x; typeof x //"undefined" x + "abc" //"undefinedabc"
12、ES3中引入undefined值,为了区分空对象指针和未初始化的变量。null == undefined的结果为true。
13、typeof正则表达式结果为"object",但是在Chrome7及之前版本和Safari 5及之前版本的结果为“function”。正则表达式的valueOf方法返回正则表达式本身。
typeof /123/ // "object" /123/.valueOf() // /123/
14、数组的长度可以修改,并且会影响数组元素的值。例如:
var arr = [1,2,3,4,5]; arr.length = 1; alert(arr[2]); //undefined
15、函数的length属性表示函数希望接受的命名参数的个数。
(function fn(a,b,c){}).length //3
16、每当读取基本类型(boolean、string、number)值的时候,后台会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。
var s1 = "abc"; s1.color = "red"; alert(s1.color); //undefined
为什么基本数据类型会有属性?其实在第二句的时候会创建临时的对应基本包装类型对象并调用指定的方法,之后会被立即销毁。
17、命名函数表达式在创建的时候,会在当前作用域最前段添加一个新的对象{func_name:refer_function_expression},然后,将作用域链添加到函数表达式的[[scope]]中,接着再删除该对象。命名函数表达式的名字只在当前函数中起作用。例如:
var x=1; if( function f(){} ){ x+=typeof f; } console.log(x); //'1undefined'
18、函数作用域链 Scope = 被调用函数函数的活动对象 + [[scope]]; [[scope]]属性在函数创建时被存储,永远不变,直到函数销毁。函数可以不被调用,但这个属性一直存在。与作用域链相比,作用域链是执行环境的一个属性,而[[scope]]是函数的属性。
19、[[scope]]属性存储着所有父环境的活动对象和变量对象。然而,用Function构造函数定义的函数例外,它的[[scope]]仅仅包含全局对象。
var name = "global"; function outer() { var name = "inner"; var getName = new Function('alert(name)'); getName(); } outer(); //global
20、 setTimeout(function(){},1000)并不表示在1s之后调用对应函数,它表示1s之后把对应的任务添加到任务队列中。如果队列是空的,那么添加的任务会立即执行;否则就要等待前面的代码先执行。因为JavaScript是单线程语言。同样的原理也存在于setInterval方法上,这可能会导致这样一种现象发生:setInterval添加的第一个任务在任务队列中等待前面代码的执行,第二个任务也被添加到了任务队列中。这样就可能会导致不必要的错误发生。因此,我们应该尽量不要使用setInterval间歇调用,而是用setTimeout超时调用来模拟间歇调用。
function test() { //... } setInterval(test,1000); //改为 function test() { setTimeout(test,1000); } test();
21、call 方法可将一个函数的对象上下文从初始的上下文改变为指定的新对象。 然而如果call方法的第一个参数为null或者undefined,那么call方法将把全局对象(浏览器中为window)作为新的对象上下文。
var x = "window_x"; var obj = { x: "obj_x", getX: function() { console.log(this.x); } }; obj.getX(); //obj_x obj.getX.call(null); //window_x obj.getX.call(undefined); //window_x obj.getX.call(window); //window_x obj.getX.call(document); //undefined
22、arguments对象中除了包含传递到函数中的参数外,还包含了length(参数个数)、callee(当前函数的引用,严格模式下不能访问)等属性。其中,arguments中保存的参数的值与对应的命名参数的值是保持同步的,但这并不表明它们共享同一个内存地址。它们的内存空间是独立的,其值通过JavaScript引擎来保持同步。注意,上面提到的同步有一个前提:对应索引值要小于传入到函数中的参数个数。即如果你传入函数2个参数,那么arguments[2]与命名参数是互不影响的。另外,arguments的length属性是由传入函数的参数决定的,就算手动为arguments添加元素,其length属性的值是不变的,除非你手动修改length的值。例如:
function fn(a,b,x) { arguments[1] = 10; console.log(b); } fn(1,2); //10 function fn(a,b,x) { arguments[2] = 10; console.log(x); } fn(1,2); //undefined function fn(a,b,x) { x = 10; console.log(arguments[2]); } fn(1,2); //undefined function fn(a,b,x) { arguments[2] = 10; arguments[3] = 20; console.log(arguments.length); } fn(1,2); //2 function fn(a,b,x) { arguments.length = 10; console.log(arguments.length); } fn(1,2); //10
23、arguments虽然有length属性,并可以通过[index]访问对应的值,但它并不是数组,然而我们可以通过Array.prototype.slice方法将它转换成数组。
function fn (a,b) { console.log(Array.isArray(arguments)); //false arguments = Array.prototype.slice.call(arguments,0); console.log(Array.isArray(arguments)); //true } fn(1,2);
24、document.write()、document.writeln()可以在页面呈现的过程中直接向页面中输出内容,但如果页面加载结束后再调用document.write()、document.writeln(),那么输出的内容将会重写整个页面。
<body> <div> Hello <script> document.write(" world!"); </script> </div> <script> window.onload = function() { setTimeout(function() { document.write("Be Matched for Marriage!!!"); //输出的内容将会重写页面 },1000); } </script> </body>
25、在Html中每个拥有id属性的元素,在JavaScript中有一个与之对应的全局变量,变量名为id的值。
<div id="myDiv"> Hello World! </div> <script> window.onload = function() { console.log(myDiv.innerHTML); // Hello World! }; </script>
26、 JavaScript语句优先原则:当{}既可以被理解为复合语句块也可以被理解为对象直接量或函数声明的时候,JavaScript将会将其理解成为复合语句块。
{a:10} //返回10,而不是对象 : 表示标签 var x = { a:10 } // {a:10}作为右值出现,不能是语句块,只能理解为对象直接量
27、JavaScript中的加法操作其实很简单,只能把数字和数字相加或者字符串和字符串相加,其它的所有类型都被转换成这两种类型相加形式。v1 + v2,转换过程如下:
1) 如果v1或者v2是对象,则转换为基本数据类型;
2) 如果v1或v2为字符串,或者原本为对象转换成基本数据类型后为字符串,则把另一个也转换成字符串,然后执行字符串链接操作得到结果;
3) 否则,转换为数字类型,返回它们的和
28、JavaScript中的加法操作中,如果操作数为对象,JavaScript引擎通过内部的抽象操作ToPrimitive()将其转换为基本数据类型:
ToPrimitive(input [, PreferredType]) PreferredType可以是Number或者String,如果为Number,则转换过程如下:
1) 调用valueOf()方法,如果valueOf()返回的结果是基本数据类型,则返回结果为转换的结果
2) 否则,调用toString()方法,如果返回结果为基本数据类型,则返回结果为转换结果
3) 否则,抛出TypeError异常
如果PreferredType为String,则转换操作的第一步和第二步的顺序会调换。
如果省略PreferredType这个参数,则PreferredType的值会按照这样的规则来自动设置:Date类型的对象会被设置为String,其它类型的值会被设置为Number。
var obj = { valueOf: function () { console.log("valueOf"); return {}; // 没有返回基本数据类型 }, toString: function () { console.log("toString"); return {}; // 没有返回基本数据类型 } } obj + "" // valueOf toString Uncaught TypeError: Cannot convert object to primitive value(…)
[] + [] // "" "" + "" {} + [] // 0 {}; +[] => {}; +"" 语句优先 [] + {} //"[object Object]" "" + "[object Object]" {} + {} // NaN {}; +{} => NaN 语句优先 ({}+{}) //"[object Object][object Object]" "[object Object]" + "[object Object]" ({}+[]) //"[object Object]" "[object Object]" + ""
29、+、-具有二义性,既可以表示一元加减操作符,又可以表示加法(减法)操作符,其中一元加减操作符为从右向左结合,加法(减法)操作符为从左向右结合。一元加减操作符的优先级高于加法(减法)操作符。++、--后置递增(递减)优先级高于一元加减操作符,++、--前置递增(递减)优先级等于一元加减操作符。
var a = 1, b = 2, c; c = a++ + - + - - - + + ++b; console.log(c); //4 console.log(a); //2 console.log(b); //3
30、JavaScript中的hoisting(悬置/置顶解析/预解析):你可以在函数的任何位置声明多个var语句,并且它们就好像是在函数顶部声明一样发挥作用。
var a = 1; function fn() { console.log(a); //undefined var a = 10; console.log(a); //10 } fn(); //相当于 function fn() { var a; console.log(a); //undefined a = 10; console.log(a); //10 } fn();
31、与hoisting类似,函数声明也有一个重要的特征——函数声明提升:执行代码之前会先读取函数声明。
fn(); function fn() { console.log(a); //undefined var a = 10; console.log(a); //10 }
因此下面这种写法就不能达到想要的效果了:
if(condition) { function fn() { alert("1"); } } else { function fn() { alert("2"); } }
大多数浏览器会返回第二个声明,忽略condition(这个问题在新版本的浏览器中做了改变);Firefox会在condition为true时返回第一个声明。但是我们可以使用函数表达式来达到我们的目的:
var fn; if(condition) { fn = function() { alert("1"); } } else { fn = function() { alert("2"); } }
32、 对于左大括号的位置每个人有着不同的偏好:同一行或者另起一行。其实放在哪里无可非议,但是由于JavaScript中分号插入机制(semicolon insertion mechanism)的存在,某些情况下,大括号的位置会产生不必要的麻烦。例如:
function fn() { return // 下面代码不会执行 { name : "Marco" } } var out = fn(); console.log(out); //相当于 function fn() { return ; { name: "Marco" } }
33、JavaScript中的ASI(自动分号插入机制)不是说在JavaScript引擎解析过程中将分号添加到代码中,而是指解析器除了分号还会以换行为基础按一定的规则作为短剧的依据,来保证解析的正确性。
产生ASI的规则:
1) 新行并入当前行,构成非法语句
2) ++、--后缀表达式作为新行开始
3) continue,break,return,throw之后
4) 代码块的最后一个语句
var name = "Marco" alert(name) //相当于 var name = "Marco"; alert(name) var a = 1,b = 2 a ++ b console.log(a); //1 console.log(b); //3 //相当于 a; ++b function fn() { return // 下面代码不会执行 { name : "Marco" } } var out = fn(); console.log(out); //undefined //相当于 function fn() { return ; { name: "Marco" } } function fn() { var x = 1, y = 2 } //相当于 function fn() { var x = 1, y = 2; }
34、不会产生ASI的规则 (承接上一点)
1)新行以 [ 开始
2)新行以 ( 开始
3)新行以 / 开始
4)新行以 . 或者 , 开始
5)新行以 + 、- 、* 、% 开始
var a = ['a1', 'a2'] var b = a [0,1].slice(1) console.log(b) //2 //相当于 var a = ['a1', 'a2'] var b = a[0,1].slice(1) //[0,1]不能解析成数组字面量,所以0,1被解析成逗号表达式,其值为1,所以相当于a[1].slice(1) console.log(b) var x = 1 var y = x (x+y).toString() // 相当于 var x = 1 var y = x(x+y).toString() //相当于将x+y的值作为参数调用函数并调用返回值的toString函数,因为x不是函数,所以会报错 Uncaught TypeError: x is not a function var m = 100 var n = m /10/.test(n) // 相当于 var m = 100 var n = m/10/.test(n) // m/10会被理解为除法运算,而第二个 / 后面希望跟的是数字继续进行除法运算 所以会报错 Uncaught SyntaxError: Unexpected token . var a = 1 var b = a .toString() // 相当于 var a = 1 var b = a.toString() //因此不会报错 var a = 1 var b = a + 1 //相当于 var a = 1 var b = a + 1 //b的结果为2
35、使用元素的contenteditable属性可以使用户可以编辑该元素。同时我们可以通过JavaScript来开启或者关闭元素的可编辑模式。
<div id="content" contentEditable></div> <button id="on">开启编辑</button> <button id="off">关闭编辑</button> <script> window.onload = function() { on.onclick = function() { content.contentEditable = "true"; } off.onclick = function() { content.contentEditable = "false"; } } </script>
36、 执行上下文的代码被分成两个阶段来处理:
1) 进入执行上下文 2) 执行代码
在进入上下文时,变量对象中将会包含以下属性:
函数所有形参(如果是在函数上下文中);所有函数声明;所有变量声明
function fn(a, b) { var c = 1; function d() {} var e = function _e() {}; (function f() {}); } fn(10); // call
进入上下文,代码执行之前,活动对象表现如下:
AO(test) = { a: 10, b: undefined, c: undefined, d: <reference to FunctionDeclaration "d"> e: undefined };
37、在执行上下文中变量、函数、形参的重名问题
1) 形参重名:形参的初始值为最后一个同名形参所对应的实参的值,如果不存在,则为undefined
function fn(a,a,a) { console.log(a); } fn(1,2,3); //3 fn(1,2); //undefined fn(1); //undefined
2) 变量重名:由于hoisting,声明被提升到开始,所以没有影响
function fn() { console.log(a); //undefined var a = 10; console.log(a); //10 var a; console.log(a); //10 } fn();
3) 函数重名:函数声明被提升,初始值为最后一次声明的函数
function fn() { console.log(1); } function fn() { console.log(2); } fn(); //2
4) 函数与形参重名:初始值为函数
function fn(a) { console.log(a); function a() { alert("Marco"); } } fn(2); //function a() { alert("Marco");}
5)变量与函数重名:初始值为函数
function fn() { console.log(a); var a = 10; function a() { alert("Marco"); } } fn(); //function a() { alert("Marco");}
6) 变量与形参重名:初始值为形参
function fn(a) { console.log(typeof a); var a = 10; } fn("Marco"); //string
7) 一个形参为arguments:arguments的初始值为对应的实参
function fn(arguments) { console.log(arguments); } fn(1,2,3); //1
8) 一个变量声明为arguments: arguments的初始值为原始值,而不是新定义的变量
function fn() { console.log(typeof arguments); var arguments = 1; } fn(1,2,3); //object
9)多个变量重名问题
在进入上下文的时候,函数声明和变量声明被提升到了作用域的开始,当他们出现重名的时候其初始值优先级为:函数声明 > 形参 > arguments > 变量声明
38、函数的形参是不可删除的
function fn(a) { console.log(a); //1 delete a; console.log(a); //1 } fn(1);
39、JavaScript中的this不是变量对象的属性,而是执行上下文环境的属性。当在代码中使用this,this的值直接从执行上下文中获取,而不会从作用域链中搜寻。
40、如何确定this的值: this的值在进入上下文时确定,并且在上下文运行期间不会改变,this的值由函数的调用方式决定。
1) 直接调用函数,this的值为undefined(严格模式)或者window(非严格模式)
2) 作为对象方法调用,this的值为该对象
3) 作为构造函数调用(new),this的值为新创建的对象
4) call、apply显式指定this的值
var name = "baby"; var fn; var obj = { name: 'Marco', getname: function() { console.log(this.name); } } obj.getname(); //Marco (fn = obj.getname)(); //baby 赋值表达式的结果为所赋的值
var name = "baby"; var fn; var obj = { name: 'Marco', getname: function() { return function() { console.log(this.name); } } } obj.getname()(); //baby
41、在定时器(setTimeout和setInterval)和requestAnimationFrame的回调函数中,无论是否使用严格模式,this的值都是全局对象。
var name = "baby"; var obj = { name: 'Marco', getname: function() { setTimeout(function(){ console.log(this.name); },1000); } } obj.getname(); //baby
42、当使用new 操作函数时,该函数就成为构造函数。new操作的过程如下:
1) 创建一个空对象obj{},this指向obj;
2) 将空对象的__proto__指向构造函数的prototype
3) 在obj的执行环境调用构造函数,如果构造函数返回值为对象,那么返回该对象;如果构造函数没有显式返回值或者返回值为基本数据类型,那么忽略原来的返回值,并将obj作为新的对象返回。
举个栗子:
function People(name) { this.name = name; } People.prototype.getName = function() { console.log("I am "+this.name); } var p = new People('Marco'); p.getName(); //I am Marco console.log(p.name); //Marco
new的模拟操作过程如下 :
var p = new People('Marco'); new People('Marco') = { var obj = {}; obj.__proto__ = People.prototype; var result = People.call(obj,"Marco"); return typeof result === 'obj' ? result : obj; }
最终的原型链为: p.__proto__->People.prototype->Object.prototype->null
再来个例子:
function f(){ return f; } new f() instanceof f;//false function f(){ return this; } new f() instanceof f;//true function f(){ return 123; } new f() instanceof f;//true
所以,我们在使用构造函数的时候尽量避免返回值,因为如果返回值为对象的话,就得不到我们想要的对象实例了。
43、全等运算符比较准则:
1) 如果数据类型不同,返回false
2) 如果两个被比较的值类型相同,值也相同,并且都不是 number 类型,返回true
3) 如果两个值都是 number 类型,当两个都不是 NaN,并且数值相同,或是两个值分别为 +0 和 -0 时,两个值被认为是全等
4) NaN === NaN 返回false
NaN === NaN //false +0 === -0 //true
44、相等运算符比较准则:
1) null == undefined //true
2) 如果数据类型相同,则按全等运算符的比较准则进行比较
3) 否则,如果一方为对象类型,则将其转换为原始值(ToPrimitive())
4) 将被比较的值转换为数字,再按全等运算符的比较准则进行比较
[] == [] //false '2' == true //false '1' == true //true var a = /123/, b = /123/; a == b //fasle a === b //false
45、in操作符的用法
1) 对于一般的对象判断某个属性是否属于对象
2) 对于数组可以指定数字形式的索引值来判断该位置是否存在数组元素
3) 使用delete删除属性之后,使用in检查该属性返回false
4) 将属性值设置为undefined,使用in检查返回true
var obj = { name: 'Marco', age: 24 } console.log('name' in obj); //true delete obj.name; console.log('name' in obj); //false obj.age = undefined; console.log('age' in obj); //true var arr = [1,2,3]; console.log(0 in arr); //true console.log(3 in arr); //false
46、如何创建稀疏数组
1) 使用构造函数Array()
2) 简单地指定数组索引值大于当前数组的长度
3) 使用delete操作符
4) 省略数组直接量中的值
5) 指定length属性
var arr1 = new Array(3); var arr2 = [1,2,3]; arr2[100] = 4; var arr3 = [1,2,3]; delete arr3[0]; var arr4 = [,,,,]; var arr5 = [1,,2,]; var arr6 = new Array(); arr6.length = 3;
47、 判断稀疏数组
1) index in array
2)forEach
var arr = [1,,2,]; console.log(0 in arr); //true console.log(1 in arr); //false console.log(3 in arr); //false console.log(arr.length); //3 arr.forEach(function(x,i) { console.log(i + " => "+x ); //0 => 1 2 => 2 });
注意:数组直接量的语法允许有可选的结尾的逗号,所以arr只有3个元素
48、连续大于号或者小于号的比较问题,从左向右依次进行,并且需要注意隐式转换
3 < 2 < 1 // true 3 > 2 > 1 //false 4 > 3 < 2 > 1 == 0 //true
49、 比较运算符
1) 如果操作数为对象,则转换为原始值(ToPrimitive())
2) 转换为原始值后如果两个操作数都为字符串,则按字典顺序比较
3) 如果至少一个不是字符串,则两个操作数都转换为数字进行数值比较,Infinity比除自己之外的其它值都大,-Infinity比除自己之外的其它值都小,-0和+0相等。如果其中一个为NaN,那么返回false
var a = [1, 2, 3], b = [1, 2, 4]; console.log( a < b); //true Array.prototype.toString = function(){ return 1; } console.log( a < b ); //false
50、函数有一个非标准的属性——name,通过它我们可以访问该函数的名字,该属性是只读的。(IE不支持)
function foo() { } var oldName = foo.name; foo.name = "bar"; console.log([oldName, foo.name] ); //["foo", "foo"] IE下返回 [undefined, "bar"]
原创文章:转载请注明文章出处 http://www.cnblogs.com/MarcoHan
很多问题没有展开讲(展开讲的话,估计只能讲一点了),可能讲的不是很详细,大家可以先看代码如果了解可以直接跳过解释。如果有问题或者错误,欢迎指正。
以上