一、对象

JavaScript简单类型有数字、字符串、布尔值、null、undefined,其他所有的值都是对象(数组、函数、正则表达式都是对象)。

数字、字符串、布尔值虽然拥有方法(包装对象),但并不是对象。

包装对象:

每当读取一个基本类型值的时候,后台会创建一个对象的基本包装类型的对象,从而能够调用一些方法来操作这些数据。

var s1 = 'abcdefg' ;
var s2 = s1.substring(2) ;

后台自动完成下列处理:

  1. 创建String类型的一个实例
  2. 在实例上调用指定的方法
  3. 销毁这个实例

所以上面代码等同于:

对象字面量

var flight = {
    airline: "Oceanic",
    number: 815,
    departure: {
        IATAL: "SYD",
        time: "2004-09-22 14:55",
        city: "Sydney"
    },
    arrival: {
        IATA: "LAX",
        time: "2004-09-23 10:42",
        city: "Los Angeles"
    }
}

检索

  • [] : flight['number']
  • . : flight.number

更新

通过赋值语句更新,如果属性名已经存在于对象中,则被替换;如果对象中没有那个属性名,则添加。

stooge['first-name'] = 'Jerome'

引用

对象赋值通过引用来传递,它们永远不会被拷贝。

var a = {
    name: 'a'
}
var b = a
b.name = 'b'
console.log(a.name)        // b

这里牵扯出 JavaScript 深拷贝和浅拷贝的问题
上例是浅拷贝使用Object.create可以进行深拷贝

var a = {
    name: 'a'
}
var b = Object.create(a)
b.name = 'b'
console.log(a.name)        // a

自定义方法深拷贝见下:

var deepCopy= function(source) { 
    var result={};
    for (var key in source) {
      result[key] = typeof source[key]===’object’? deepCoyp(source[key]): source[key];
   } 
   return result; 
}

此时 var b = deepCopy(a) 得到的 b 就和 a 没有引用关系,即修改 b 不会影响 a

原型

每个对象都连接到一个原型对象,并且从中继承属性。所有通过对象字面量创建的对象都连接到 Object.prototype 这个JavaScript中标准的对象。

创建一个对象时,可以选择某个对象作为它的原型:

var o = {o1:1,o2:function(){alert(1)}}
function F(){}
F.prototype = o
var f = new F()

反射

使用 hasOwnProperty 检查属性是否是对象独有的,它并不会检查原型链。

flight.hasOwnProperty('number');    //true

枚举

for in 可以遍历对象中所有的属性名(见深拷贝部分)

删除

delete 可以删除对象属性,不会触及原型链中的任何对象

减少全局变量污染

最小化使用全局变量的一个方法是创建唯一一个全局变量:

var App = {}
App.stooge = {
    "first-name": "Joe",
    "last-name": "Howard"
}
App.flight = {
    airline: "Oceanic",
    number: 815
}

减少全局变量污染另一个办法是使用闭包进行信息隐藏

二、函数

函数包含一组语句,是Javascript的基础模块单元,用于代码复用、信息隐藏和组合调用。

一般来说,所谓编程就是将一组需求分解成一组函数与数据结构的技能。

函数对象

函数就是对象,对象是键值对的集合并且拥有一个连到原型对象的隐藏连接。对象字面量产生的对象连接到 Object.prototype ,函数对象连接到 Function.prototype (该原型对象本身又连接到 Object.prototype)。

var add = function(a,b){
    return a + b;
}

函数表达式包含四部分:

  1. 保留字 function
  2. 函数名,可以被省略(匿名函数)
  3. 参数,逗号分隔
  4. 花括号中的语句

函数表达式允许出现在任何允许表达式出现的地方,函数也可以被定义在其他函数中。一个内部函数可以访问自己的参数和变量,同时它也能方便地访问它被嵌套在其中的那个函数的参数和变量。通过函数表达式创建的函数对象包含一个连到外部上下文的连接,被称为闭包。

调用

函数在调用的时候有两个附加参数:thisarguments

this 是调用上下文,值取决于函数调用的模式。

1.方法调用模式

一个函数被保存为对象的一个属性时,即为一个方法。当一个方法被调用时,this 被绑定到该对象。

每个函数在创建时附有两个附加的隐藏属性:

  • 函数上下文
  • 实现函数行为的代码

每个函数对象在创建时也会带一个 prototype 属性,它的值是一个拥有 constructor 属性且值为该函数的对象。

函数表达式

函数对象可以通过函数表达式来创建:

var dog = {
    name : 'xxx' ,
    leg:{
        sum : 4 ,
        move:function(){
            console.log(this) ; //Object,是leg对象,而不是dog对象,下面证明了
            alert(this.name) ; //underfined
            alert(this.sum) ; //4
        }
    }
}
dog.leg.move();

2.函数调用模式

函数仅仅当做函数来调用时,this 被绑定到全局对象。

var a = 111 ;
function t1(){
    var a = 1
    function t2(){   
        console.log(this.a)    //111,这其实很不合理,应该指向t2的。
    }
    t2()
}
t1()

这其实是语言设计上的一个错误,倘若语言设计正确,当内部函数被调用时,this 应该仍然绑定到外部函数的 this 变量。

3.构造器调用模式

如果一个函数前面带上 new 调用,那么将创建一个隐藏连接到该函数的 prototype 成员的新对象,同时 this 将被绑定到那个新对象上。

function Dog(name){
    this.name = name ;
}
Dog.prototype.cry = function(){
    alert(this.name)
}
var dog1 = new Dog('xxx');
dog1.cry(); // 'xxx'

4.Apply/Call调用模式

apply 接受两个参数,第一个是将被绑定给this的值,第二个就是一个参数数组。

call 与 apply 相同,不过第二个参数不是数组。

var dog = {
    leg : 4 ,
    color:'yellow'
}
var color = 'red' ;
function t(){
    alert(this.color) ;
}
t(); // red , 因为指向this在函数中调用指向window
t.call(dog); //yellow , 把t()的作用域指向了dog

再来说说 arguments,它是一个类数组对象(拥有length属性,但缺少所有数组方法)。通过它可以访问函数调用时传递给函数的参数列表。

返回

一个函数调用时,将暂停当前函数的执行,传递控制权和参数给新函数。它从第一个语句开始执行,并在遇到关闭函数体的 } 时结束。使得函数把控制权交还给调用该函数的程序部分。

return 语句可用来使函数提前返回,当 return 执行时,函数立即返回而不再执行余下的语句。一个函数总是会返回一个值,如果没有指定返回值,则返回 undefined 。

如果函数前加上 new 来调用,且返回值不是一个对象,则返回this(该新对象)。

异常

抛出异常

function add(a,b){
    if(typeof a!=='number' || typeof b!=='number'){
        throw {
            name: 'TypeError',
            message: 'add needs numbers'
        }
    }
    return a + b;
}

throw 语句中断函数的执行,抛出一个 exception 对象,该对象包含可识别异常类型的 name 属性和一个描述性的 message 属性。

该 exception 对象将被传递到一个 try 语句的 catch 从句

try{
    add('seven')
}catch(e){
    console.log(e.name)
    console.log(e.message)
}

如果在 try 代码块中抛出异常,控制权就跳转到其 catch 从句。

给类型增加方法

Number.prototype.integer = function(){
    return Math[this < 0 ? 'ceiling' : 'floor'](this) //this指向实例
}

var num = 10/3
console.log(num.integer())    ;    // 3

作用域

作用域空间那个值变量和参数的可见性和生命周期,对程序员来说很重要,因为它减少了命名冲突,并且提供了自动内存管理。

JavaScript没有块级作用域,却有函数作用域:定义在函数中的参数和变量在函数外部是不可见的,而且在一个函数中的任何位置定义的变量在该函数中任何地方都可见。

下面这个例子与以对象字面量初始化对象不同,通过调用一个函数形式去初始化对象,返回一个对象字面量。此函数定义了一个 val 变量,该变量对 addVal 和 getVal 总是可用的,但函数的作用域使得其对其他程序来说是不可见的。

// 从设计模式的角度来说这是模块模式
var o = (function(){
    var val = 0;
    return {
        addVal: function(){
            val += 1
        },
        getVal: function(){
            console.log(val)
        }
    }
})()

联想到之前我做的一个小游戏,是20秒内完成任务,使用 restTime 做倒计时变量。后来同事把restTime修改了,成绩贼高。最后我就是用这种办法把 restTime像 val 一样隐藏了起来。

闭包

作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments)。

var o = function(){
    var val = 0;
    return {
        addVal: function(){
            val += 1
        },
        getVal: function(){
            console.log(val)
        }
    }
}
var oo = o()
oo.addVal()
oo.addVal()
oo.getVal() // 2

当调用 o 时,返回一个包含addValgetVal的新对象,该对象的引用保存在 oo 中。虽然 o 返回了,但是 oo 的addValgetVal有访问 val 的特权,它们可以访问被创建时所处的上下文,这就是闭包。

模块

可以使用函数和闭包来构造模块。模块是一个提供接口却隐藏状态与实现的函数或对象。

具体见『作用域』部分-模块模式

三、继承

当一个函数对象被创建时,Function 构造器产生的函数对象会运行类似这样的代码:

this.prototype = {constructor: this}

每个函数都会得到一个 prototype 对象,其值是包含一个 constructor 属性且属性值为该新函数对象。该 prototype 对象是存放继承特征的地方。

四、数组

区分数组和对象

一个常见的错误是在须使用数组时使用了对象,在须使用对象时使用了数组。其实规则很简单:当属性名是小而连续的整数时,用数组,否则就用对象。

JavaScript中数组 typeof 返回是 object,这并不能区分数组和对象。

var is_array = function(value){
    return value && 
        typeof value === 'object' &&
        typeof value.length === 'number' &&
        typeof value.splice === 'function' &&
        !(value.propertyIsEnumerable('length'))
}
  • 首先,我们判断这个值是否为真,我们不接受 null 和其他为假的值。
  • 其次,我们判断这个值的 typeof 运算结果是否是 object 。对于对象、数组和null来说,将得到 true
  • 第三,我们判断这个值是否有一个值为数字的 length 属性,对于数组是 true ,对于对象则为 false
  • 第四,判断这个值是否包含一个 splice 方法。对于数组,返回 true
  • 最后,我们判断 length 属性是否是可枚举的

这真的很复杂,实际上,我一直是这样用的,什么类型都能检测,堪称万能:

var toString = Object.prototype.toString
function isObject(obj) {
  return toString.call(obj) === "[object Object]"
}
function isString(obj) {
  return toString.call(obj) === "[object String]"
}
function isArray(obj) {
  return toString.call(obj) === "[object Array]"
}
function isFunction(obj) {
  return toString.call(obj) === "[object Function]"
}

五、方法

Array

concat(item...)

返回一个新数组,并不会修改原数组

var a1 = [2,3]
var a2 = [332,12]
console.log( a1.concat(a2) ); //[ 2, 3, 332, 12 ]

join(separator)

把一个 array 构造成一个字符串,并用 separator 作为分隔符把它们连接在一起。

pop(item...)

移除数组中最后一个元素并返回该元素

push(item...)

将一个或多个元素添加到数组尾部,会修改原数组

reverse()

反转数组中元素的顺序,会修改原数组,返回当前数组

shift()

移除数组中第一个元素

slice(start,end)

从start开始,到end为止(不包括end,可选,默认值是length)复制数组

sort(comparefn)

对数组中的内容排序,并不能给数字排序,因为默认比较函数是假定要被排序的元素都是字符串。

比较函数接受两个参数,并且如果两个参数相等返回0,如果第一个参数应该排在前面,则返回一个负数,如果第二个参数应该排在前面,则返回一个正数。

// a 比 b小时返回 -1,而且根据上述规则 a 会排在前面
// 所以这是从小到大的排序

var arr = [1,123,341,34,123]
arr.sort(function(a,b){
    if(a==b){
        return 0
    }else{
        return a < b ? -1 : 1 
    }
})

splice(start,deleteCount,item...)

splice 从数组中移除一个或多个元素,并用新的item代替他们。

start是从数组中移除元素的开始位置,deleteCount是要移除的个数。

会修改原数组,返回一个包含被移除元素的数组。

var arr = [23,3,23,2]
var b = arr.splice(0,1)

console.log(arr)     ;     //[ 3, 23, 2 ]
console.log(b) ;     //[ 23 ]

deleteCount 为0时,则为添加新元素:

var arr = [23,3,23,2]
var b = arr.splice(1,0,'aa')

console.log(arr)        // [ 23, 'aa', 3, 23, 2 ]
console.log(b)        // []

deleteCount 与 item的个数相等时,则为替换:

var arr = [23,3,23,2]
var b = arr.splice(1,1,'aa')

console.log(arr)        //[ 23, 'aa', 23, 2 ]
console.log(b)        //[ 3 ]

unshift(item...)

将item从数组头部插入数组

Function

apply(thisArg,argArray)

见 『Apply/Call调用模式』

Number

toFixed(fractionDigits)

把这个 number 转换成一个十进制形式的字符串。可选参数 fractionDigits 控制其小数点后的数字位数。

Math.PI.toFixed(); //3
Math.PI.toFixed(2); //3.14
Math.PI.toFixed(4); //3.1415

toPrecision(precision)

同 toFixed ,参数控制有效数字的位数

toString()

将number转换成字符串

Object

hasOwnProperty(name)

只检查此对象中的属性,原型链中得同名属性不会被检查。如果存在此属性则返回 true

String

charAt(pos)

返回在字符串中pos处的字符

charCodeAt(pos)

返回不是一个字符串,而是以整数形式表示的字符码位

concat(string...)

与其他字符串连接起来构造一个新字符串,不常用,因为 + 也能满足需求

indexOf(searchString,pos)

在字符串内查找另一个字符串 searchString,如果被找到,则返回第一个匹配字符的位置,否则返回 -1 。

可选参数 pos 设置从字符串的某个指定位置开始查找。

lastIndexOf(searchString,pos)

与indexOf类似,不同从末尾开始查找

slice(start,end)

复制字符串的一部分构造一个新的字符串

split(separator,limit)

把字符串分割成片段创建数组,limit可限制被分割的片段数量。
一个有意思的技巧:

new Array(11).join('0').split('') //生成10个元素为0的数组

toLowerCase()

将字符串中所有字母转化为小写

toUpperCase()

将字符串中所有字母转化为大写

六、糟粕

这一部分用来吐槽JS这门语言设计上不周到的地方

全局变量

共三种方法定义全局变量:

  1. 脱离任何函数var语句 var foo = value
  2. 直接添加一个属性到全局对象中,全局对象是所有全局变量的容器。在web中,全局对象是 window : window.foo = value
  3. 使用未声明的变量,这被称为隐式的全局变量:foo = value

之前说过,可以通过 创建一个全局变量 和 闭包 减少全局变量污染(注意,只是减少,没办法避免,总要有暴露出来的变量,不要钻牛角尖)。

作用域

没有块级作用域,只有函数作用域

自动插入分号

JavaScript 有一个机制,会试图通过自动插入分号来修正有缺损的程序。它有可能会掩盖更为严重的错误。

return
{
    status: true
}

看起来是返回一个对象,但是自动插入分号让它返回了undefined,这样可以避免:

return {
    status: true
}

typeof

typeof并不能正确地检测数据类型:

typeof null ; //object

所以使用 Object.prototype.toString.call(null) 这个办法就好,万能的!