首页 > 代码库 > 你根本不会Javascript(1)——类型、值和变量

你根本不会Javascript(1)——类型、值和变量

文原载于szhshp.org/tech/2017/02/18/JavaSprite.html

转载请注明

类型、值和变量

包装对象和原始值

ECMAScript 有 5 种原始类型(primitive type)

  1. Undefined
  2. Null
  3. Boolean
  4. Number
  5. String

  • 基本类型(null, undefined, bool, number, string)应该是值类型,没有属性和方法

内置对象

Javascript有一系列内置对象来创建语言的基本功能,具体有如下几种

Boolean

Boolean 对象表示两个值:true 或 false。当作为一个构造函数(带有运算符 new)调用时,Boolean() 将把它的参数转换成一个布尔值,并且返回一个包含该值的 Boolean 对象。如果作为一个函数(不带有运算符 new)调用时,Boolean() 只将把它的参数转换成一个原始的布尔值,并且返回这个值,如果省略 value 参数,或者设置为 0-0null""falseundefined 或 NaN,则该对象设置为 false。否则设置为 true(即使 value 参数是字符串 false)。

Boolean 对象包括 toString 和 valueOf 方法, Boolean 最常用于在 条件语句中 true 或 false 值的简单判断,布尔值和条件语句的组合提供了一种使用 Javascript 创建逻辑的方式。

Number

Number对象是一个数值包装器,该对象包含几个只读属性:

  • MAX_VALUE:1.7976931348623157e+308 //Javascript能够处理的最大数
  • MIN_VALUE:5e-324 //Javascript能够处理的最小数
  • NEGATIVE_INFINITY:-Infiny //负无穷
  • POSITIVE_INFINITY:Infinity //正无穷
  • NaN:NaN //非数字

Number 对象还有一些方法,可以用这些方法对数值进行格式化或进行转换:

  • toExponential //以指数形式返回 数字的字符串表示
  • toFixed //把Number四舍五入为指定小数位数的数字
  • toPrecision //在对象的值超出指定位数时将其转换为指数计数法
  • toString //返回数字的字符串表示
  • valueOf //继承自object

String

String 对象是文本值的包装器。除了存储文本,String 对象包含一个属性和各种 方法来操作或收集有关文本的信息,String 对象不需要进行实例化便能够使用。

String 对象只有一个只读的length属性用于返回字符串的长度。String对象拥有很多方法:

  • charAt
  • charCodeAt
  • concat
  • fromCharCode
  • indexOf
  • lastIndexOf
  • match
  • replace
  • search
  • slice
  • split
  • substr
  • substring
  • toLowerCase
  • toUpperCase

包装对象

除了上面三个对象,Javascript还拥有Date、Array、Math等内置对象,这三个经常显示使用,所以非常熟悉,知道了内置对象就可以看看上面例子是怎么回事儿了。

只要是引用了字符串的属性和方法,Javascript就会将字符串值通过new String(s)的方式转为内置对象String,一旦引用结束,这个对象就会销毁。所以上面代码在使用的实际上是String对象的length属性和indexOf方法。

同样的道理,数字和布尔值的处理也类似。null和undefined没有对应对象。

既然有对象生成,能不能这样:

var s=‘this is a string‘;
s.len=10;  //创建了一个临时的String对象,随即销毁
alert(s.len);  //第三行代码又会创建一个新的临时对象, 并没有返回10,而是undefined!

a = 1;
a.s = 2;
a.s// 一样undefined
  • 第二行代码只是创建了一个临时的String对象,随即销毁。
  • 第三行代码又会创建一个新的临时对象,自然没有len属性。
  • 这个创建的临时对象就成为包装对象

如何区分原始对象和包装对象

Javascript会在必要时将包装对象转换为原始值因此显示创建的对象和其对应的原始值常常但不总是表现的一样。==运算符将原始值和其包装对象视为相等;但===全等运算符将他们视为不等;另外通过typeof运算符可以看到原始值和包装对象的不同。

不可变的原始值和可变的对象引用

Javascript中的原始值(undefinednull、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或突变)一个原始值。对数字和布尔值来说显然如此——改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,Javascript是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。

//字符串原始值修改不了
var str = "abc";
str[0] = "d";
console.log(str[1]="f"); //>>f
console.log(str[0]); //>>a
console.log(str); //>>abc

原始值的比较是值的比较, 但是对象是引用类型, 因此可以看成是地址的比较

var a = {‘x‘ : 1}, b = {‘x‘ : 1};
alert(a === b);//false, 值相同但是地址不同

var c = [1], d = [1];
alert(c === d);//false, 同上

对象转换为原始值

  • 对象转换为到布尔值比较简单,所有对象到布尔都是true,包括包装类new Boolean(false)是一个对象而不是原始值,它将转换为true
  • 对象到数字对象到字符串比较复杂一些。注意这里讨论的是本地对象,不包含宿主对象(例如浏览器定义的对象)

所有对象继承了以下两个转换方法:

toString()

它的作用是返回一个反映这个对象的字符串。默认的toString()方法并不会返回一个有趣的值。

很多类定义了特定版本的toString()方法:

  • 数组的toString() 
    方法将每个数组元素转换为一个字符串,并在元素之间添加逗号合并成结果字符串
  • 函数类的toString() 
    方法返回这个函数的实现定义的表示方式。通常是将用户定义的函数转换为Javascript源代码字符串
  • 日期类toString() 
    返回一个可读的日期和时间字符串。
  • RegExp类的toString() 
    将返回RegExp对象转换为表示正则表达式直接量字符串。
[1,2,3].toString()//=>`1,2,3`
(function(x){f(x);}).toString()//=>` function(x){\nf(x);\n}`

/\d+/g.toString()//=>`/\\d+/g`
newDate(2010,0,1).toString() //=>`Fri Jan 01 2010 00:00:00 GMT-0800(PST)`

valueOf()

对象是复合值,而且大多数对象无法真正表示一个原始值。数组、函数和正则表达式简单地继承了这个默认方法,调用这些类型的实例的valueOf()方法只是简单地返回对象本身。日期类的valueOf方法会返回一个内部表示:1970年1月1日以来的毫秒数

通常情况下对象是通过toString()和valueOf()方法,就可以做到对象到字符串和对象到数字的转换。

对象到字符串转换逻辑

  1. 如果具有toString()方法,则调用这个方法,如果它返回一个原始值,js将其转换为字符串,并返回这个字符结果。
  2. 如果没有toString()或者这个方法并不返回一个原始值,那么js会去调用valueOf()。如果有调用它,如果返回值是原始值。则将其转换成字符串。
  3. 如果没有toString()或valueOf()获得一个原始值,因此会抛出一个类型错误异常。

逻辑很清晰,先试试toString()能否获得正确的值,如果不行再试试valueOf(),否则报错。

对象到数值的转换

  1. 如果对象具有valueOf()方法,后者返回一个原始值,则Javascript 将这个原始值转换为数字并返回这个数字
  2. 否则,如果对象具有toString() 方法,后者返回一个原始值,则js将这个原始值转换返回
  3. 否则,js报类型错误。

以上是翻译的原文,可能有些难读,不过其实也很容易理解:先试试valueOf()然后再试试toString(),否则报错。

运算符使用时的数值转换

  • Javascript里面的+运算符可以进行加法或者字符串连接操作。如果其中一个操作数是对象,那么就会将对象转为原始值而不是执行对象到数字的转换。

  • ==操作符类似,如果对象和一个原始值进行比较, 那个对象也会转换成一个原始值。另外,日期类型是一种特殊的情况,日期是Javascript语言核心中唯一的预先定义类型。对于所有非日期对象,对象到原始值的转换基本上是对象到数字的转换(首先调用valueOf()),日期对象则使用对象到字符串的转换模式。并且,通过valueOf()或者toString()返回的原始值将本地直接使用而不会被强制转换为数字或字符串。

  • ==一样,<运算符以及其它关系算术运算符也会做到对象到原始值得转换,但是如果是日期对象则会使用上方粗体字的特殊的逻辑。因此除了日期对象之外的任何对象比较都会先尝试调用valueOf, 然后调用toString。不管得到的原始值是否直接使用,它都不会进一步被转换为数字或字符串。

  • +==!=关系运算符是唯一执行特殊的字符串到原始值的转换方式的运算符。其它运算符到特定类型的转换很明确,而且对日期对象来讲也没有特殊情况。例如-运算符把它的两个操作数都转换为数字。下面演示日期对象各种运算的结果:

var now=new  Date(); 

console.log(typeof (now+1)); //string +号把日期转换为字符串
//对于加号操作符,我们会将操作对象转换为字符串然后进行计算

console.log(typeof (now-1)); //number -号把对象到数字的转换
//对于减号操作符,我们会将操作对象转换为数字然后进行计算

console.log(now==now.toString()); //true 
//对于比较操作符,我们一般会优先转换为原始值再进行比较
//但是日期类型例外!和日期对象相比较会转换成字符串再进行比较

console.log(now>now-1);//true >把日期转换为数字

变量声明

  • 变量未赋值前的初始值是undefined,不是null,不是null,不是null
  • 我们不会给变量声明类型, 因此将一个原本是数字的变量重新赋给字符串的值也是合法的,但是一般要避免这种情况出现。

重复的声明和遗漏的声明

  • 使用var语句多次声明同一个变量不仅是合法的,而且也不会造成任何错误。
  • 如果重复的声明有一个初始值,那么它担当的不过是一个赋值语句的角色。
  • 如果尝试读一个未声明的变量的值,Javascript会生成一个错误。
  • 如果尝试给一个未用var声明的变量赋值,Javascript会隐式声明该变量。
  • 但是要注意,隐式声明的变量总是被创建为全局变量,即使该变量只在一个函数体内使用。局部变量是只在一个函数中使用,要防止在创建局部变量时创建全局变量(或采用已有的全局变量),就必须在函数体内部使用var语句。无论是全局变量还是局部变量,最好都使用var语句创建。

变量作用域

  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
  • 一般情况下,window 对象的内置属性都拥有全局作用域,例如window.namewindow.locationwindow.top 等等。
  • 尽管在全局作用域编写代码时可以不写 var 语句,但声明局部变量时则必须使用 var 语句。
scope = "global";           // 声明一个全局变量,甚至不用 var 来声明
function checkscope2() {
    scope = "local";        // 糟糕!我们刚修改了全局变量
    myscope = "local";      // 这里显式地声明了一个新的全局变量
    return [scope, myscope];// 返回两个值
}
console.log(checkscope2()); // ["local", "local"],产生了副作用
console.log(scope);         // "local",全局变量修改了
console.log(myscope);       // "local",全局命名空间搞乱了
  • 函数定义是可以嵌套的。
var scope = "global scope";         // 全局变量
function checkscope() {
    var scope = "local scope";      //局部变量 
    function nested() {
        var scope = "nested scope"; // 嵌套作用域内的局部变量
        return scope;               // 返回当前作用域内的值
    }
    return nested();
}
console.log(checkscope());          // "nested scope"

函数作用域和声明提前

在一些类似 C 语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称为块级作用域(block scope),而 Javascript 中没有块级作用域。Javascript 取而代之地使用了函数作用域(function scope),变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

在如下所示的代码中,在不同位置定义了变量 ij 和 k,它们都在同一个作用域内,这三个变量在函数体内均是有定义的。

function test(o) {
    var i = 0; // i在整个函数体内均是有定义的
    if (typeof o == "object") {
        var j = 0; // j在函数体内是有定义的,不仅仅是在这个代码段内
        for (var k = 0; k < 10; k++) { // k在函数体内是有定义的,不仅仅是在循环内
            console.log(k); // 输出数字0~9
        }
        console.log(k); // k已经定义了,输出10
    }
    console.log(j); // j已经定义了,但可能没有初始化
}

Javascript 的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。有意思的是,这意味着变量在声明之前甚至已经可用。Javascript 的这个特性被非正式地称为声明提前(hoisting),即 Javascript 函数里声明的所有变量(但不涉及赋值)都被「提前」至函数体的顶部,看一下如下代码:

var scope = "global";
function f() {
    console.log(scope);  // 输出"undefined",而不是"global"
    //因为在这个作用域里面局部变量已经覆盖了全局变量,但是还没有执行到
    var scope = "local"; // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
    console.log(scope);  // 输出"local"
}

你可能会误以为函数中的第一行会输出 "global",因为代码还没有执行到 var 语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量遮盖了同名全局变量。尽管如此,只有在程序执行到 var 语句的时候,局部变量才会被真正赋值。因此,上述过程等价于:将函数内的变量声明“提前”至函数体顶部,同时变量初始化留在原来的位置:

function f() {  var scope; // 在函数顶部声明了局部变量 console.log(scope); // 变量存在,但其值是"undefined" scope = "local"; // 这里将其初始化并赋值 console.log(scope); // 这里它具有了我们所期望的值 }

在具有块级作用域的编程语言中,在狭小的作用域里让变量声明和使用变量的代码尽可能靠近彼此,通常来讲,这是一个非常不错的编程习惯。由于 Javascript 没有块级作用域,因此一些程序员特意将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处。这种做法使得他们的源代码非常清晰地反映了真实的变量作用域。

作为属性的变量

当声明一个Javascript全局变量时,实际上是定义了全局对象的一个属性。

当使用var声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过delete运算符来删除。可能你已经注意到,如果你没有使用严格模式并给一个未声明的变量赋值的话,Javascript会自动创建一个全局变量。以这种方式创建的变量是全局对象的正常可本会属性,并可以删除它们:

var a =1;
b =2;
this.b2 = 3;
delete a;    //不可删除
delete b;    //可删除
delete this.b2  //可删除

Javascript全局变量是全局对象的属性,这是在ECMAScript 5规范称为“声明上下文对象。Javascript可以允许用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。然而,这些局部变量对象存在的观念是非常重要的。

你根本不会Javascript(1)——类型、值和变量