虽然Javascript 的大多语法都是基于传统语言去编写的,但还是有一些设计缺陷,这些缺陷在浏览器的表现怪异,令人费解。在这里,我花了一些时间去整理这些怪异现象,并对相关知识点作相应解释。

该篇文章主要包括了数据类型、变量和作用域、函数、数组和对象五大部分,最后还介绍了其他一些特殊的函数,文章较长:

数据类型

== 隐式类型转换比较符

在javascript中,== 对两边的数据作比较之前,会先对数据进行转换成同一数据类型,再作比较。

0==""; // true
1=="1"; // true
false=="false"; // false
false=="0"; // true
false==undefined; // false
false==null; // false
null==undefined; // true
null==""; // false

new Number(1) == 1; //true
10 == '+10'; // true
10 == '-10'; // false
10 == '010'; // true
10==010 ; //false
1==01 ; //true

=== 不转换严格比较符

而 === 不会做任何比较,而是直接比较数据。

0===""; // false
1==="1"; // false
false==="false"; // false
false==="0"; // false
false===undefined; // false
false===null; // false
null===undefined; // false
null===""; // false

new Number(1) === 1; //false
10 === '+10'; // false
10 === '-10'; // false
10 === '010'; // false
10===010 ; //false
1===01 ; //true

由此可见,在日常的编程中,为了避免潜在的风险,我们建议使用严格比较符(===)。

将一个字符串转化为数字类型,我们可以在字符串前加一个一元加性(或减性)操作符:

var str="5";
typeof(+str); // number

而如果要将一个数字转化为字符串,则可以将数字加上一个空字符串:

var num=5;
typeof(""+num); // string

typeof

typeof通常用于检测数据类型,除了返回四种基本数据类型(number,string,boolean,undefined)外,typeof还会返回object ,函数则会返回function。其中数组、正则、null等都会返回object,而且null表示一个空的object 。

instanceof

对于都是返回object的数据,如果想要明确知道它是数组还是正则,typeof显然无能为力。ecmascript 引入了instanceof运算符,它用来判断指定对象是否是由某个类构造的实例,然后返回一个Boolean值。

Number instanceof Number;//false
new Number(1) instanceof Number;// true
[1,2] instanceof Array; // true
new Array(1,2) instanceof Array; // true

变量和作用域

变量的定义

Javascript中变量必须使用var来定义,如果函数中的变量未使用var,则该变量会变成全局变量。

function varFn(){
  a=5;
}

function getVar(){
  alert(a);
}
varFn();
getVar(); // 5

delete对变量和属性的处理

在ECMAScript中有三种类型的可执行代码:全局代码(Global code)、函数代码(Function code)和Eval code。
delete操作符可以用来删除对象的属性,它返回一个boolean值,删除成功的情况下为true,否则为false。Delete只能删除自由属性,不能删除继承属性(即原型对象上的属性),也不能删除var声明的变量(即所有用var声明的语句,无论普通变量、函数还是对象),同样也不能删除函数以及它的参数。观察以下代码:

var a=1;
delete a;
a; // 1 a未被删除,因为var 声明

eval("var b= 2;");
delete b;
b; // b is not defined 被删除

c=3;
delete c;
c; // c is not defined 被删除

var obj={
  d:4
}
delete obj.d;
obj.d; // undefined 被删除

使用var声明一个变量时,创建的这个属性是不可配置的,故无法delete。而未使用var声明,就相当于创建了window下的一个属性,所以可以删除。

另外需要强调的是,delete是删除属性的唯一操作符。给属性设置null或undefined 是相当于对象属性的重新赋值,而非真正的删除属性。

命名冲突

一段代码中,如果存在命名相同的变量或者命名相同的函数,则后面同名变量和函数会覆盖前面的同名变量和函数(即函数没有重载);但若出现名称相同的一个变量和一个函数时,若变量初始化了(就算初始化的值为undefined),则在该代码片段的结果中为为变量的值,否则为函数。

var a=1;
function a(){
  return 1;
}
a; // 1 声明了变量,且初始值为1,返回1

var a;
function a(){
  return 1;
}
a; // function a(){return 1;} 声明了变量,但未初始化,返回函数体

var a=1;
function a(){
  return 1;
}
a(); // a is not a function 数字无函数调用方法

声明提升及作用域

var声明的语句和函数声明都会提升到当前作用域的顶部(值为undefined),这部操作也就是在所谓”预编译”完成的。但赋值表达式只有到运行到此行时,才会赋值成功,否则它的值为undefined。

declarationFn(); // undefined
var a=1;
function declarationFn(){
  alert(a);
  var a=2;
}

代码中declarationFn为函数声明。所以declarationFn能正常运行,然后开始在declarationFn这个作用域里查找a,因为var声明的语句会提升到当前作用域的顶部,但赋值语句只有在执行到该行时,才会赋值成功,否则其值为undefined。所以运行declarationFn的结果为undefined。

作用域

首先作用域分为全局作用域和局部作用域。全局作用域也可称为window下的属性,函数中变量的访问是从局部作用域开始查找的,如果找到则返回该变量。如果未找到,则继续查找全局作用域,如果还是没找到,最后将报错。

因此名称相同的变量,函数内部的优先级比在函数外部的高。同样因为作用域的关系,子函数可以访问父函数定义的变量,函数可以访问全局变量,反之则不成立。

var a="global";
function scopeFn(){
  var a="local";
  function testScope(){
    return a;
  }
  testScope();
}

a; // global
scopeFn(); // undefined

不要以为scopeFn()的值会为local,其实不然。虽然testScope在scopeFn中执行了,但return a 是testScope的返回值,返回到scopeFn中只有一个变量a,但a在函数scopeFn中没做任何操作(没有return出去),所以我们可以把运行结果看成是这样的:

function scopeFn(){
  a; // a=local , 无返回
}

而非这样:

function scopeFn(){
  return a; // a=local , 返回到全局环境中
}

代码中scopeFn本身并无返回值,再由上面分析可知,scopeFn运行的结果只能是undefined。

函数

函数声明和函数表达式

我们都知道函数名是指向函数对象的指针,而函数的定义一般分为两种形式,即函数声明和函数表达式:

首先来看函数声明,函数声明会率先得到解析器的读取,也就是说的函数声明提升,它会在当前作用域的顶部。所以,我们可以在函数定义之前就调用函数:

testDeclaration(); // 1
function testDeclaration(){
  var a=1;
  alert(a);
}

而对于函数表达式中,就好像变量赋值的过程,只有当执行到该行testExpression才会真正会解析:

testExpression(); // 报错 ,testExpression不是一个函数
var testExpression=function(){
  var a=1;
  alert(a);
}

函数怪异写法

还有一种定义函数的方法,即用Function来构造函数。该方法可以接受任意个参数,但最后一个始终会当做函数体来运行:

var add= new Function("a","b","return a+b");

函数变量的访问

检测一个函数能否访问某个变量,不是看函数执行的位置,而是函数定义的位置。如:

function returnA(){
  return a;
}
function intA(){
  var a=1;
  return returnA;
}
intA()(); // a is not defined

函数返回值

如果函数本身没有使用return返回任何值(注意这里的本身,无论其子函数是否使用return),则函数默认的返回值是undefined。

function returnFn(){
  var a=1;
}
returnFn(); // undefined

function returnFn(){
  var a=1;
  return a;
}
returnFn(); // 1

在下列运行的函数中,会先弹出1,在弹出undefined(函数默认返回值)。

function returnFn(){
  var a=1;
  alert(a);
}

returnFn(); // 1

关于this

在全局作用域下,this指的是window,比如全局下定义一个变量或函数,则可以看出是window下的属性或方法。

function testThis01(){
  alert(this===window);
}
testThis01(); // true 相当于window.testThis()

而在构造函数中,this则指向的是新创建的实例对象:

function testThis01(){
  alert(this===window);
}

var testThis02=new testThis01();//false

如果是对象调用,则this指的是调用的对象:

var obj={
  testThis03:function(){
    alert(this===obj);
  }
}

obj.testThis03(); // true

当然,我们也可以使用call或者applay 来动态的设置this的执行:

var a=1;
var obj={
  a:2,
  testThis04:function(){
    return function(){
      return this.a;
  }
}
}

alert(obj.testThis04()()); //1
alert(obj.testThis04().apply(obj,[obj])); // 2 这里也可以用call

总结一句,this的指向与作用域无关,它只和调用函数的对象有关,如window.testThis01()中的window、obj.testThis04()中的obj。即谁调用函数,则this指向谁。

函数参数

每个函数都有length和prototype属性,函数的length即为它接受形参的个数。

function lengthFn(a,b,c,d){
  return a+b+c+d;
}
lengthFn.length; // 4

在函数内部,有两个特殊的对象:arguments和this。其中arguments是一个传入函数中所有实参的类数组对象,因为它是函数内部的对象,因此无法在函数外部访问:


function testArguments (a,b,c,d){
  return arguments.length;
}
testArguments (1,2,3,4,5,6); // 6

另外,我们还可以在函数内部通过arguments去修改实参:

function changeArguments (a,b,c) {
  arguments[2] = 10;
  alert(c);
}
changeArguments (1, 2, 3); // 10

闭包

先来看一段代码:

function testClosure01(){
  var a=1;
}

alert(a) ; // 报错 a is not defined

如你所知,外面变量是无法访问其他函数的内部变量。因为作用域问题,testClosure01函数执行完后会,它就会释放占用的内存,并销毁它自身的变量对象,这样函数中定义的变量也不复存在,所以外部不能访问函数内部定义的变量。

再看以下代码:

function testClosure02(){
  var a=1;
  function closureFn(){
    alert(a);
  }
  return closureFn;
}
var testClosuerVar=testClosure02()(); // 1

因为testClosure02()()被赋值给了testClosuerVar,而testClosure02返回时是它里面的子函数 closureFn,testClosure02()()就相当于在testClosure02中调用closureFn,而closureFn由引用了testClosure02中定义的变量a,因此testClosuerVar可以访问到a。

所谓的闭包,就是外部变量可以引用其他函数的作用域,因为引用关系,那么它的变量对象将不会被销毁,内存也不会被释放,然后便可访问函数内部定义的变量。闭包相当于在函数的内部和外部建立了一座桥梁。

callee

前面提到了arguments主要用于保存函数的实参,但它还有一个callee的属性,该属性指向拥有这个arguments对象的函数,借用经典的阶乘函数来看一下:

function factorial(num){
  if(num<=1){
    return 1;
  }
  else{
    return num*arguments.callee(num-1);
  }
}

在这里,我们用到了arguments.callee(num-1),而它指的就是函数factorial(num)。那么为什么不用它本身函数名呢?试想一下,如果用的是函数名,而当我们改变此函数名时,那么内部函数名也需要进行更改。但是如果我们使用arguments.callee(num-1),则消除了这种函数名的紧密耦合,因为arguments.callee指的就是包含它的函数。

caller

caller保存着调用当前函数的函数引用,在全局作用域中,它将返回null。它的定义让人理解起来有点晦涩,来观察以下代码:

function outer(){
  inner();
}

function inner(){
  alert(arguments.callee.caller); // 等价 inner.caller
}

outer(); // function outer(){inner()}

从运行的函数中可以看到,由于outer调用了inner,因此inner.caller就返回了outer的源代码。

数组

数组的定义

关于javascript数组定义的方式有很多种,可以采用new Array的方式,也可以使用数组字面量,如下:

var arr1=new Array(1,2,3,4,5) ; // [1,2,3,4,5] 长度为5
var arr2=new Array(5); // [,,,,] 长度为5,每项的值均为undefined
var arr3=new Array("5") // ["5"] 长度1
var arr4=[1,2,3,4,5] ; // [1,2,3,4,5] 长度为5,与arr1 一样

改变数组长度

我们可以轻松的改变一个数组的长度,改变后的长度大于(或小于)数组原来的长度,除非人为设置,否则数组新增项(或大于新长度的原有项)的值均为undefined;

当然,我们也可以瞬间清空一个数组,只需要将它的length设为0;

var arr=[1,2,3,4,5];
arr.length=10;
arr ; //[1,2,3,4,5,,,,,]
arr[8] ;// undefined

arr.length=2;
arr ; // [1,2]
arr[4]; // undefined

介绍两个非常有用的方法:

arr.sort(function(num1,num2){return num1-num2}) //升序
arr.sort(function(){return Math.random()>0.5?-1:1}) //随机

对象

引用

在javascript中,number、string、boolean、undefined是属于基本类型,它们是具有固定大小,存储在栈内存中的数据。
而对象、数组和函数则属于引用类型,引用类型的数据是不固定长度的,它们保存在堆内存中。

var arr = new Array();

arr存储在栈中它指向于new Array()这个数组对象,而new Array()则是存放在堆中的。
基本数据类型是按值来访问的。而引用类型则是按引用来访问的,引用类型的变量名其实是一个指向该引用存储位置的指针。

基本类型表现为只是简单的赋值,赋值后的变量与原始变量无关:

var a=1;
var b=a;
b; //1
a=2;
b; // 1

而引用类型则不是,赋值给它们的只是对象的一个引用。我们改变的只是对象的引用,而对象名是指向引用的指针,所以新对象会随着原始对象变化而改变:

var arr1=[1,2,3];
var arr2=arr1;
arr2; // 1 ,2 ,3
arr1[3]=4; // 改变原始数组
arr2; // 1, 2 , 3 , 4

原型

在javascript中,除了函数,原型也是比较复杂的。我通常把对象比喻成人,那么原型对象就是人的本能,比如人生下来就会哭会闹会眨眼等,这些与生俱来的功能就可以理解成对象的原型。

比如字符串有substring、indexOf,数组有toString、push,日期对象有getDay,getSeconds等方法。这些对象原本就有的方法都存在原型对象中。

创建的每个函数都有prototype(原型)属性,这个属性是一个指针,指向函数的原型下

function Person(){}

Person.prototype.name="yi";
  Person.prototype.showName=function(){
  alert(this.name);
}

var person1=new Person();
person1.showName(); // yi

var person2=new Person();
person2.showName(); // yi

由图中可知,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。Person.prototype(原型对象)除了包含constructor属性外,还包括在对象的prototype上添加的其他属性。Person1和Person2都包含一个内部属性,该属性指向的是Person.prototype,而与构造函数无直接关系。

我们可以通过isPrototypeOf()来确定对象的属性是否在原型对象是否存在,而检查对象的属性是否来自实例,则可用 hasOwnProperty();

function Person(){}

Person.prototype.name="yi";
Person.prototype.showName=function(){
  alert(this.name);
}

var person1=new Person();
var person2=new Person();

alert(Person.prototype.isPrototypeOf(person1)); // ture

person2.name="chen";

alert(person1.name); // yi 来自原型
alert(person2.name); // chen 来自实例

alert(person1.hasOwnProperty("name")); // false
alert(person2.hasOwnProperty("name")); // true

delete person2.name;
alert(person2.name); // yi 来自原型

当代码读取对象的属性时,首先会从对象实例中开始查找,如果找到则返回该属性值。若没找到,则继续搜索指针对象的原型对象,然后返回该属性值。比如代码中的person1和person2在查找那么属性时,在person2没有重新设置name属性值时,都会查找到原型中的name,即多个对象实例共享原型所保存的属性和方法。

重置person2.name,则返回的事实例中的name值。当删除实例中name属性时,则又返回Person.prototype中的值。这里需注意,原型中的属性和方法不可删除。

判断一个属性不是来自实例,而是来自原型时,我们可以封装如下方法:

function hasPrototypeProperty(obj,name){
  return obj.hasOwnProperty(name)&&(name in obj);
}

因为name in obj始终返回ture,而只有当属性是来自实例时,obj.hasOwnProperty(name)才为ture。所以,当hasPrototypeProperty(obj,name)返回false时,则属性来自原型,反之则来自实例。

为了书写简便,我们把prototype对象里的属性和方法集中在一起,类似对象字面量的写法:

function Person(){}

Person.prototype={
  name:"yi",
  showName:function(){
    alert(this.name);
  }
}

var person1=new Person(); // yi

alert(person1.constructor==Person); //false

但constructor属性不再指向Person了。我们知道每创建一个函数,同时也会创建prototype对象,然后这个对象会自动获取constructor属性。以上的书写方式,本质上完全重写了prototype对象,因此constructor属性指向了新的对象。

我们可以动态的设置constructor的指向:

Person.prototype={
  constructor:Person,
  name:"yi",
  showName:function(){
    alert(this.name);
  }
}

下图展示了重写prototype前后的变化:

重写前:

重写后:

重写后的原型对象切断了现有原型与任何之前已经存在的对象实例直接的联系;它们引用的仍然是最初的原型。

关于javascript原型,这里介绍的主要是实例与原型间的关系,若需深入理解它,则必须多加练习和扩展。

其它

return

一般情况来说,在函数里return后面的语句是不会执行的,如下:

function testReturnA(){
  var a=1;
  return a;
  alert(a); // 不执行
}

但是也有一些特殊情况是例外的,比如try catch finally 语句如下:

function testReturnB(){
  try{
    alert(0)
    return 0;
  }
  catch(error){
    return 1;
  }
  finally{
    return 2;
  }
}
alert(testReturnB()) //0 2

另外,return必须与返回的内容在同一行,否则javascript的解析器会自动在return后插入行尾分号,然后返回值为undefined ,如下:

function testReturnC(){
  return
  2;
}
testReturnC(); // undefined

parseInt

parseInt函数用来返回一个整数,但遇到非数字则停止截取,返回之前的数字。

var str1="123X456";
parseInt(str1); // 123

以0开头后面接数字,在低版本的IE中会解析成八进制,超过八进制则返回0。而现代浏览器则返回具体数字:

var str2="091";
parseInt(str2) ;// 91 或者 0

如果数字之前含有一个或者多个0,如0后面无数字则只返回一个0。若0之后还有数字,则会返回0后面的数字,而不返回前面的0。

var str3="00x2";
var str4="000010";
parseInt(str3); //0
parseInt(str4); //10

另外parseInt无视数据前后的空格。最后要注意parseInt的第二个参数,即返回的进制数。

var str5=" 10 ";
parseInt(str5); //10

浮点数溢出

二进制的浮点数不能正确地处理十进制的小数,这是ecmascript遵循二进制浮点数算术标准(IEEE 754)而导致的结果,所以会有:

0.1+0.2!=0.3
0.1+0.2 // 0.30000000000000004

我们可以通过先把小数转换成整数,做运算后再除相应的倍数即可。

(0.1*10+0.2*10)/10 // 0.3

for in

由于使用for in 循环对象属性的过程,会遍历该对象原型链上的所有属性,这显然不是我们想要的。Javascript定义了hasOwnProperty方法,该方法主要用于检测对象的自定义属性,于是如果我们只是想要获取自定义的属性,可以通过以下方法:

for(var i in obj){
  If(obj.hasOwnProperty(i)){
    Console.log(i+";"+obj[i]);
  }
}

undefined 与 null

在javascript中,这两个特殊的值都表示为空。若对(undefined==null)进行求值,我们会得到true。

如果对它们使用typeof,undefined会返回undefined,而null则返回object。

如果从数据类型去区分它们的话,undefined表示没有赋值的基本数据类型,null表示没有赋值的引用数据类型。

eval

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码,类似将字符串转为了javascript对象:

var str="var a=1;alert(a)";
str; // "var a=1;alert(a)";
eval(str) ;// 1

值得注意的事,eval函数执行的结果只在当前作用域有效。

var a=1;
function testEval(){
  var a=2;
  eval("a=3")
  return a;
}
testEval(); // 3
a; // 1

可是eval有一个很奇怪的特性,就是当它不是直接调用,而是将它赋给一个变量再调用时,会出现意外的结果。

var a=1;
function testEval(){
  var a=2;
  var oEval=eval;
  oEval("a=3");
  return a;
}
testEval(); // 2
a; // 3

可以看到,此时的eval就好像在全局作用域下调用。

而在平常的使用中,eval的功能主要是用于解析一段ajax返回的数据,如json。由于eval强大的执行功能,因此一般情况下不推荐使用。