JS权威指南读书笔记

一、类型,值和变量

1.构造函数

函数用来初始化一个新建的对象,我们称为构造函数。每个构造函数定义了一类对象(由构造函数初始化的对象组成的集合)

数组类,函数类,日期类,正则类,错误类

2.Math 对象的函数运算:

Math.pow(2,53)  //2 的 53 次幂

Math.round(.6)  //四舍五入

Math.ceil(.6)  //向上求整

Math.floor(.6)  //向下求整

Math.abs(-5)  //求绝对值

Math.sqrt(3)  //3 的平方根

Math.log(10)  //10 的自然对数

Math.exp(e)  //e 的三次幂

3.二进制浮点数和四舍五入错误

数字具有足够的精度,并可以及其近似于 0.1,但是有时候不能准确的表达也会出问题。

var x = .3 - .2;
var y = .2 - .1;
x == y;  // => false


var zero = 0;
var negz= -0;
zero = negz // =>true : 0 和-0 值相同
1/zero = 1/negz; // => false : 正无穷大和负无穷大不相等
没有办法通过 x==NaN 来判断变量 x 是否是 NaN。应该用 x!=x 来判断,当 x=NaN 时为 true。函数 isNaN()的作用与此类似,若参数是 NaN 或一非数字值,返回 true。isFinite(),参数不是 NaN,Infinity 或-infinity 时返回 true

4.Date()构造函数

var then = new Date(2011, 0, 1)  //2011 年 1 月 1 日

var later= new Date(2011, 0, 1, 17, 10, 30)  //2011 年 1 月 1 日 5:10:30pm

var now = new Date()  //当前日期和时间

var elasped = now - then  //计算时间间隔的毫秒数

later.getFullYear()  //2011

later.getMonth()  //0 从零计算月份

later.getDate()  //1 从 1 计算天数

later.getDay()  //1 得到星期几 0 代表星期日 1 代表星期一

later.getHours()  //当地时间 17:10pm

later.getUTCHours() //UTC 表示小时的时区

5.JavaScript 转义字符

\o   NUL 字符   

\b  退格符   

\t   水平制表格 

\n  换行符   

\v   垂直制表符   

\f   换页符 

 \r   回车符 

\"   双引号   

\'   单引号   

\\   反斜线   

6.字符串方法

var s = "hello, world"

s.charAt(0)   //第一个字符 h

s.charAt(s.length - 1)   //最后一个字符 d

s.substring(1, 4)    //第 2~4 个字符

s.slice(1, 4)    //第 2~4 个字符

s.slice(-3)    //最后 3 个字符

s.indexOf("l")   //字符 l 第一次出现的位置

s.lastIndexOf("l")   //字符 l 最后一次出现的位置

s.split(",")  // ["hello", "world"]分割成子串

s.replace(/l/g, "L")  //全文字符替换

s.toUpperCase() // "HELLO, WORLD" 全部大写

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

undefined,null,布尔值,数字和字符串为不可变的原始值

例如

var s = "hello, world";
s.replace(/l/g, "L");
s.toUpperCase();
s 

看似对 s 进行了很多操作,其实他们只不过是返回了一个新的字符串,并没有对 s 进行改变

对象可以修改

var o = { x: 1};
o.x = 2;
o.y = 3;
console.log(o);


var a = [1,2,3];
a[0] = 0;
a[3] = 4;
console.log(a);

对象的比较并非值的比较,即便属性和值相同,两个对象也不相等

//数组和对象都是可修改的

var o = { x: 1};
o.x = 2;
o.y = 3;
console.log(o);


var a = [1,2,3];
a[0] = 0;
a[3] = 4;
console.log(a);


// 具有两个相同属性的两个对象永不相等
var o = {x: 1}, p = {x: 1};
console.log(o === p); // => false


//两个单独的空数组永不相等
var a = [], b = [];
console.log(a === b);// => false


/当 c 数组引用 a 数组时,c 数组的修改会影响 a 一起修改,且 c 和 a 相等
var c = a;
c[0] = 1;
console.log(a[0]); // => 1
console.log(a === c); // => true

要对对象进行复制,要对其每个属性进行复制,比较两个对象或者数组是否相等也要比较各个属性及其值

//循环赋值

        var a = ['a', 'b', 'c']
        var b = [];
        for(var i = 0; i < a.length ; i++) {
            b[i] = a[i];
        }

8.数字转化为字符串 3 种方法

var n = 123456.789;

//根据小数点后指定位数转化字符串

n.toFixed(0);   //"123456"

n.toFixed(2);   //"123456.79"

n.toFixed(5);   //"123456.78900"

//指数计数法,指定小数点后有几位

n.toExponential(1);   //"1.2e+5"

n.toExponential(3);   //"1.235e+5"

//指定有效数字

n.toPrecision(4);   //"1.235e+5"

n.toPrecision(7);   //"123456.8"

n.toPrecision(10);   //"123456.7890

9.转化为数字

Number(), parseInt(), parseFloat()

二、表达式和运算符

1.与运算(&)

计算规则:两个计算的二级制数 相同位为 1 结果为 1 否则为 0

1 & 1 = 1;

1 & 0 = 0;

0 & 1 = 0;

0 & 0 = 0;

2.或运算(|)

计算规则:相同位置的两个二进制数 有 1 结果就是 1 否则为 0

0 | 0 = 0;

0 | 1 = 1;

1 | 0 = 1;

1 | 1 = 1;

3.异或运算(^)

计算规则:相同为 0 相异为 1

0 ^ 0 = 0;

0 ^ 1 = 1;

1 ^ 0 = 1;

1 ^ 1 = 0;

4.非运算(~)

计算规则:将操作数所有位数取反。

~0x0F = 0xFFFFFFF0 或 -16

5.左移 (<<)

运算规则: 按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的补零

数学意义: 在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以 2 的 1 次,左移 n 位就相当于乘以 2 的 n 次方

例如:3 << 2,则是将数字 3 左移 2 位 计算过程:3 << 2 首先把 3 转换为二进制数字 0000 0011,然后把该数字高位(左侧)的两个零移出,其他的数字都朝左平移 2 位,最后在低位(右侧)的两个空位补零。则得到的最终结果是 0000 1100,则转换为十进制是 12。

6.带符号右移 (>>)

运算规则: 按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的补零

数学意义: 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方。(只取商不取余数)

7.不带符号右移 (>>>)

与带符号右移相同,但高位总是补零,负号不补 1

8.in 运算

如果指定的属性在指定的对象或其原型链中,则 in 返回 true。

1、如果第二个运算数为对象,则 in 运算符用来检测第一个运算数是否是第二个运算数的属性名。

var point = { x: 1, y: 1 };

console.log("x" in point);           // => true:对象有一个名为"x"的属性

console.log("z" in point);;         // => false:对象不存在名为"z"的属性

console.log("toString" in point);   // => true:对象继承了 toString()方法

2、如果第二个运算数为数组,则 in 运算符用来检测第一个运算数是否为数组包含的索引之一。

var data = [7, 8, 9];
console.log("0" in data);           // => true 数组包含元素"0"
console.log(1 in data);             // => true 数字转换为字符串
console.log(3 in data);             // => false 没有索引值为 3 的元素

9.instanceof 运算

用来在运行时指出对象是否是特定类的一个实例。instanceof 通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例。

var d = new Date();                 //通过 Date()构造函数创造一个新对象
console.log(d instanceof Date);     // => true d 是由 Date()创建的
console.log(d instanceof Object);   // => true 所有的对象都是 Object 的实例
console.log(d instanceof Number);   // => false d 不是一个 Number 对象;

var a = [1, 2, 3];
console.log(a instanceof Array);    // => true a 是一个数组
console.log(a instanceof Object);   // => true 所有的数组都是对是对象;
console.log(a instanceof RegExp);   // => false 数组不是正则表达式;

10.eval()函数

可以使用 eval 函数计算结果、表达式 、符串或数值的值。

可以构造一个字符串,然后将它传递给 eval 函数,就像字符串是实际表达式一样。eval 函数计算字符串表达式并返回其值。 例如,eval("1 + 1") 返回 2。

var geval = eval;                   //这里调用 geval => 间接调用 eval;
var x = "global", y = "global";

    function fn() {                     //此函数直接调用 eval()
        var x = "local";
        eval("x = x +'changed'");
        return x;

    }

    function gn() {                     //此函数间接调用 eval()
        var y = "local";
        geval("y = y + 'changed'");
        return y;

    }
    console.log(fn(), x);               //这里会输出 localchanged global,这里的 localchanged 就是 fn 中返回的结果,而全局属性 x 依然为 global;
    console.log(gn(), y);               //这里会输出 local globalchanged, 这里 lcoal 就是 gn 中返回的结果 ,因为 gn()函数中是对全局 eval=>geval 进行了修改所以 y 为 globalchanged

11.三元运算符(?:)

操作语句 ?true:fale;

var a = 20;
var b = 10;
var max = a > b ? a : b; //如果 a>b 执行 a,否则执行 b;
console.log(max);

12.typeof、delete、void 运算符

(1).typeof 用来显示对象类型

var num = 10;
console.log(typeof num); // number
var str = 'pink';
console.log(typeof str); // string

(2).delete

delete 是一个一元操作符,它用来删除对象属性或者数组元素.就像赋值 递增 递减运算符一样.但是 delete 也有副作用,它是用来删除操作的,不是返回一个值.

var o = {x:1,y:2};
console.log('x' in o);          //true
console.log(o.x);//1
console.log(delete o.x);     //true 删除 x 属性
console.log('x' in o);          //false,可以用 in 检测属性是否被删除
console.log(o.x);                //undefined
var a = [1,2,3,4];
console.log(a.length);         //4
console.log(a[2]);             //3
console.log(delete(a[2]));     //true
console.log(2 in a)            //false 元素 2 已经不存在数组中了
console.log(a.length);         //4,并没有删除 a[2],仅仅是把 a[2]的值置为 undefined
console.log(a[2]);             //undefined,验证上面说法

对于不同的变量声明方式来说,会用 var 关键字声明和 function 创建全局对象属性不能被删除(不可配置属性)

var a = 10;
b = 20;
console.log(a);             //10
console.log(b);             //20
console.log(delete a);      //false
console.log(delete b);      //true
console.log(a) ;            //10
console.log(b) ;            //报错

(3).void

void 会正常进行计算,但是会忽略返回结果并返回 undefined

三、语句

1.判断语句与循环语句

(1) 判断语句

if 语句
i = 1;
j = 2;
if (i == j)
  console.log("i equs j");
else
  console.log("i dosent equal j");
switch 语句
switch (n) {
            case 1: //如果 n ===1 从这里开始
                //执行代码块 1
                break;  //停止执行 switch 语句
            case 2: //如果 n ===2 从这里开始
                //执行代码块 2
                break;
            case 3:
                //执行代码块 3
                break;
            default: //如果所有条件都不匹配
                //执行代码块 4
                break;
        }

(2) 循环语句

while
while (expression)
       statement

在执行 while 语句之前,javascript 解释器首先计算 expression 的值。表达式为 expression 是真值的时候则循环执行 statement,注意,使用 while(true)则会创建一个死循环。

do/while

它是在循环的尾部而不是顶部检测循环表达式,这就意味这循环体至少执行一次。

 do 
       statement 
 while(expression);
for

在这类循环中,计数器的三个关键操作是初始化、检测和更新。for 语句就将这三部操作明确声明为循环语法的一部分,各自使用一个表达式来表示。

for (initialize; test; increment)
     statement
var i, j;
for (i = 0, j = 10; i < 10; i++, j--)
    console.log(i * j);

需要注意的是,for 循环中的那三个表达式中的任何一个都可以忽略,但两个分号必不可少。如果省略 test 表达式,那么将是一个死循环。同样和 while(ture)类型,死循环的令一种写法是 for(;;)。

for/in
for (variable in object)
    statement

variable 通常是一个变量名,也可以是一个可以产生左值的表达式或者一个通过 var 语句声明的变量。总之是一个适用于赋值表达式左侧的值。object 是一个表达式,这个表达式的计算结果是一个对象。同样,statement 是一个语句或语句块,它构成了循环的主体。

for/in 循环则是用来方便的遍历对象成员属性:

for (var p in o) //将属性的名字赋值给变量 p
    console.log(o[p]); //输出每一个属性的值

需要注意的是,只要 for/in 循环中,varibale 的值可以当做赋值表达式的左值,它可以是任意表达式。每次循环都会计算这个表达式,也就是说每次循环它计算的值可能不同。例如,可以使用下面的这段代码将所有对象属性复制到一个数组中:

var o = {x: 1,y: 2,z: 3};
var a = [],i = 0;
for (a[i++] in o) /*empty*/;

javascript 数组只不过是一种特殊的对象,因此,for/in 循环可以像枚举对象属性一样枚举数据索引。例如在上面的代码之后添加这段代码,就可以枚举数据索引 0,1,2:

for(i in a)console.log(i)

其实,for/in 循环并不会遍历对象的所有属性,只有“可枚举”(enumerable)的属性才会遍历到(参照 书中6.7)。由于 javascript 语言核心所定义的内置方法就不是“可枚举的”。比如,所有的对象都有 toString(),但 for/in 循环并不枚举 toString()这个属性。除了内置的方法之外,还有很多内置对象的属性是不可枚举的(nonenumberable)。而代码中定义的所有属性和方法都是可枚举的。对象可以继承其它对象的属性,那些继承自定义属性也可以使用 for/in 枚举出来。

如果 for/in 的循环体删除了还未枚举的属性,那么这个属性将不会再枚举。如果循环体定义了对象的 新属性,这些属性通常也不会枚举到(不过。javascript 有些实现可以枚举那么些在循环体中增加的属性)。

属性枚举的顺序

ECMAScript 规范并没有指定 for/in 循环按照何种顺序来枚举对象的属性。但实际上,主流的浏览器厂商 javascript 实现是按照属性定义的先后顺序来枚举简单对象的属性,先定义的属性先枚举。如果使用对象直接量的形式创建对象,则将按照直接量中属性的出现顺序枚举。

在下面的情况下,枚举顺序取决于具体的实现(并非交互):

  1. 对象具有整数数组索引的属性
  2. 使用 delete 删除了对象已有的属性
  3. 使用 Object.defineProperty()或者类似的方法改变了对象属性
  4. 对象继承了可枚举属性

    2.throw 语句

    所谓异常是当发生了某种异常情况或错误时产生的一个信号。

    抛出异常就是用信号通知发生了错误或异常状况。

    捕获异常时指处理这个信号,即采取必要的手段从异常中恢复。

    在 JS 中,当产生运行时错误或者程序使用 throw 语句时就会显示地抛出异常。使用 try/catch/finally 语句可以捕获异常。

     function fac(x){
            if(x<0) throw new Error("x 不能是负数");  //如果输入参数是非法的,则抛出一个异常。JS 解释器会立即停止当前正在执行的逻辑,并跳转至就近的异常处理程序。
            for(var f=1; x>1; x--)  //否则,计算出一个值,并正常返回它    
    f*=x
           return f;
          };

    3.try/catch/finally 语句

         try/catch/finally 语句是 JS 的异常处理机制。

        其中 try 从句定义了需要处理的异常所在的代码块。catch 从句跟随在 try 从句之后,当 try 块内某处发生了异常时,调用 catch 内的代码逻辑。catch 从句后跟随 finally 块,后者中放置清理代码,不管 try 块中是否异常,finally 块内的逻辑总会执行。

         try{

             //通常来讲,这里的代码会从头执行到尾而不会产生任何问题

            //但有时会抛出一个异常,要么是由 throw 语句直接抛出异常

           //要么是通过调用一个方法间接抛出异常

          }

          catch(e){

            //当且仅当 try 语句块抛出了异常,才会执行这里的代码

            //这里可以通过局部变量 e 来获得对 error 对象或者抛出的其他值的引用

            //这里的代码块可以基于某种原因处理这个异常,也可以忽略这个异常

            //还可以通过 throw 语句重新抛出异常

           }

          finally{

           //不管 try 语句块是否抛出了异常,这里的逻辑总是会执行,终止 try 语句块的方法有:

             1.正常终止,执行完语句块的最后一条语句

             2.通过 break、continue 或 return 语句终止

             3.抛出一个异常,异常被 catch 从句捕获

             4.抛出一个异常,异常未被捕获,继续向上传播

               }

         例:

     try{
               //要求用户输入一个数字
                  var n = Number(prompt("请输入一个正数",""));
                  //假设输入是合法的,计算这个数的阶乘
                 var f=fac(n);
                  //显示结果
                  alert(n+"!="+f);
           }
            catch(ex){
              //如果输入不合法,将执行这里的逻辑
               alert(ex);  //输入-3,出现 alert("x 不能是负数");
           }

    4.其他语句类型

    剩余的三种javascript语句:width,debugger和use strict。

    with语句

    with语句用于临时扩展作用域链(一个可以按序检索的对象列表,通过它可以进行变量名解析)。这条语句将object添加到作用域链的头部,然后执行statement,最后把作用域链恢复到原始状态。

    with (Object)
      statement

    尽量避免使用with语句。

    在对象嵌套层次很深的时候通常会使用with语句来简化代码的编写。例如,在客户端JavaScript中,可能会使用类似下面这种表达式来访问一个HTML表单中的元素:

    document.forms[o].address.value

    如果这种表达式在代码中多次出现,则可以使用with语句将form对象添加至作用域链的顶层:

    with(document.forms[o]){
      name.value = "";
      address.value = "";
      email.value = "";
    }

    这种方法减少了大量的输入,不再为每个属性名添加document.forms[o]前缀。不使用with语句可以写成这样:

    var f = document.forms[o];
    f.name.value = "";
    f.address.value = "";
    f.email.value = "";

    只有在查找标识符的时候才会用到作用域链,创建新的变量的时候不使用它。with语句提供了一种读取属性的方式,但它不能创建属性。

    debugger

    debugger语句通常什么也不做。然而,在调试程序可用并运行的时候,javascript解释器将会(非必须)以调试模式运行。实际上,这条语句产生一个断点(breakpoint),javascript代码执行会停止在断点的位置,这时可用使用调速器输出变量的值,检查调用栈等。

    假如加上调用函数f()的时候使用了未定义的参数,因此f()抛出一个异常,但无法定位到到底哪里出了异常。为了有助于调试这个问题,需要修改f():

    function f(o){
        if (o === undefined) debugger; //这段代码用来临时调试
        .....       //函数的其它部分
    }

    这时候,当调用f()没有传入参数,程序将停止执行,这时候通过调用调速器检测调用栈并找出错误的原因。

    在ECMAScirpt5中,debugger语句已经正式加入到专门语言里,但在很长的一段时间里,主浏览器的厂商已经将其实现了。注意,可用的调速器是远远不够的,debugger语句不会启动调试器。但如果调试器已经在运行,这条语句才会真正产生一个断点。例如,使用Firefox插件firebug,首先启动firebug,这样debugger语句才能工作。

    use strict

    "use strict"是ECMAScript5引入的一条指令。指令不是语句(但非常接近于语句)。

    "use strict"的目的是指定代码在严格条件下执行。

    严格模式下你不能使用未声明的变量

    “use strict”指令和普通的语句之间有两个重要的区别:

  1. 它不包含任何语言的关键字,指令仅仅是一个包含一个特殊字符串直接量的表达式(可以是使用单引号也可以使用双引号),对于那些没有实现ECMAScript5的JavaScript解释器来说。他只是一条没有副作用的表达式语句,他什么也没做。将来的ECMAScript标准希望将use用做关键字,这样就可以省略引号了。
  2. 它只能出现在脚本代码的开始或者函数体的开始、任何实体语言之前。但他不必一定出现在脚本的首行或函数体内的首行,因为 "use strict"指令之后或之前都可能有其他字符串直接量表达式语句,并且JavaScript的具体实现可能将他们解析为解释器自有的指令。在脚本或者函数体内第一条常规语句之后字符串直接量表达式语句只当做普通的表达式语句对待;它们不会当做指令解析,他们也没有任何副作用。
  3. 在严格模式中禁止使用with语句。
  4. 在严格模式中,所有的变量都要先声明,如果给一个未声明的变量、函数、函数参数、cantch从句参数或全局对象的属性赋值,将会抛出一个引用错误异常(在非严格模式中,这种隐式的全局变量的方法是给去全局对添加一个新属性)。
  5. 在严格模式中,调用的函数(不是方法)中的一个this 值是undefined。(在非严格模式中,调用的函数中的this值总是全局对象)。可以利用这种特性来判断JavaScript实现是否支持严格模式:
  6. 同样,在严格模式中,当通过call()或apply()来调用函数时,其中的this值就是通过call()或apply()传入的第一个参数(在非严格模式中,null和undefined值被全局对象和转换为对象的非对象值所替代)。
  7. 在严格模式中,给只读属性赋值和给不可扩展的对象创建新成员都将抛出一个类型错误异常(在非严格模式中,这些操作只是简单的操作失败,不会报错)。
  8. 在严格模式中,传入eval()的代码不能在调用程序所在的上下文中声明变量或定义函数,而在非严格模式中是可以这样做的。相反,变量变量和函数的定义在eval()穿创建的新作用域中,这个作用域在eval()返回时就启用了。
  9. 在严格模式中,函数里的arguments对象(8.3.2)拥有传入函数值的静态副本。在非严格模式中,arguments对象具有“魔术般”的行为,arguments里的数组元素和函数参数都是指向同一个值的引用。
  10. 在严格模式中,当delete运算符后跟随非法的标识符(比如变量、函数、函数参数)是,将会抛出一个语法错误异常(在非严格模式中,这种delete表达式什么也没做,并返回false)。
  11. 在严格模式中,试图删除一个不可配置的属性将抛出一个类型错误异常(在非严格模式中,delete表达式操作失败,并返回false)。
  12. 在严格模式中,在一个对象直接量中定义两个或多个同名属性将产生一个语法错误(在非严格模式中不会报错)。
  13. 在严格模式中,函数声明中存在两个或多个同名的参数将产生一个语法错误(在非严格模式中不会报错)。
  14. 在严格模式中是不允许使用八进制整数直接量(以0位前缀,而不是0x为前缀)的(在非严格模式中某些实现是允许八进制整数直接量的)
  15. 在严格模式中,标识符eval和arguments当做关键字,他们的值是不能更改的。不能给这些标识符赋值,也不能把他们声明为变量、用做函数名、用做函数参数或用做catch块的标识符。
  16. 在严格模式中限制了对调用栈的检测能力,在严格模式的函数中,arguments.caller和arguments.callee都会抛出一个类型错误异常。严格模式的函数同样具有caller和arguments属性,当访问这两个属性时将抛出类型错误异常(有一些JavaScript的实现在非严格模式里定义了这些非标准的属性)

5. 小结

四、对象

JavaScript对象可以看作是属性的无序集合,每个属性就是一个键值对,可增可删。

JavaScript中的所有事物都是对象:字符串、数字、数组、日期,等等。

JavaScript对象除了可以保持自有的属性外,还可以从一个称为原型的对象继承属性。对象的方法通常是继承的属性。这种“原型式集成”是JavaScript的的核心特征。

1.创建对象

(1)对象直接量表示法创建对象

这是最简单的对象创建方式,对象直接量由若干key:value键值对属性组成,属性之间用逗号分隔,整个对象用花括号括起来。

var empty = {}; //不包含任何属性的对象
var point = { x: 3, y: 5 }; //包含两个属性的对象
var point2 = { x: point.x + 1, y: point.y + 1 }; //属性值可以是表达式
var book = {
    "main title": "JavaScript", //属性名有空格,必须用字符串表示
    "sub-title": "The Defintive Guide", //属性名有连字符,必须用字符串表示
    "for": "all audiences", //属性名是保留字,必须用字符串表示
    author: { //这个属性的值是一个对象
        firstname: "David",
        surname: "Flanagan"
    }

ECMAScript 5版本中,使用保留字属性名可以不用引号引起来。对象直接量最后一个属性后的逗号自动忽略。

(2)通过关键字创建对象

关键字new用来创建并初始化对象,后面跟一个构造函数。JavaScript语言核心中原始类型都包含内置构造函数,下面是内置对象创建演示。

var o = new Object(); //创建一个空对象,等价于 0={}
var a = new Array(); //创建一个空数组
var d = new Date(); //创建一个代表当前时间的Date对象
var r = new RegExp("js"); //创建一个正则表达式对象

除了这些内置构造函数,使用自定义构造函数来初始化新对象也很常见。

(3)原型

介绍第三种方法之前需要先简单了解“原型”的概念。每一个JavaScript对象(null除外)都有一个关联对象,并且可以从关联对象继承属性。这个关联对象就是所谓的“原型”,类似于C#中的基类。

所有通过对象直接量和构造函数创建的对象都可以通过Object.prototype获得原型对象的引用。没有原型的对象为数不多,Object.prototype就是其中之一。

普通对象都有原型,比如Array数组对象的原型是Array.prototype。同时,内置构造函数都具有一个继承Object.prototype的原型。因此,通过new Array()创建的数组对象的属性同时继承至Array.prototype和Object.prototype,当对象出现多继承关系时,那么这一系列链接的原型对象就被称作“原型链”。

(4)使用Object.create()函数创建对象

Object.create(Object[,Properties])是ECMAScript 5版本出现的一个静态函数,用来创建对象。它接收两个参数:第一个是要创建对象的原型;第二个是可选参数,用来描述对象属性。

使用它创建对象,只需传入所需原型对象即可:

vara = Object.create({ 'isLock': true });  //为对象a指定一个原型
console.log(a.isLock); //=> true  o继承原型对象属性isLock
console.log(a.hasOwnProperty('isLock')); //=> false  验证isLock并非o的自有属性

创建一个普通的空对象,需要传入参数Object.prototype:

var b = Object.create(Object.prototype);

可以通过传入参数null来创建没有原型的对象,该类对象不会继承任何东西:

var b = Object.create(null); //该对象不包括任何对象的基础方法

通过原型创建对象,可以使任意对象可继承,这是一个强大的特性。比如可以防止程序无意修改不受控制的对象。程序不直接操作对象,而是操作通过Object.create()创建的继承对象。

 //inherit()返回了一个继承自原型对象p属性的新对象
 //这里是有ECMAScript5中的Object.create()函数(如果存在的话)
 //如果不存在Object.create,则使用其他方法
function inherit(p) {
        if(p==null)    throw TypeError();//p是一个对象,但不能是null
        if(Object.create){    //如果Object.create()存在,直接使用它
            return Object.create(p);
        }
        var t = typeof p ;
        if(t !=="object" && t!=="function") throw TypeError();
        function f(){};//定义一个空构造函数
        f.prototype = p ;   //将其原型属性设置为p 
        return new f() ;    //使用f创建p的继承对象
    }

注意:inherit()并不能完全代替Object.create(),它不能通过传入null原型来创建对象,而且不能接收可选的第二个参数。

inherit()函数的其中一个用途就是防止库函数无意间(非恶意地)修改那行不受你控制的对象。不是将对象直接作为参数传入函数,而是将它的继承对象传入函数。当函数读取继承对象的属性时,实际上读取的是继承来的值。如果给继承对象的属性赋值,则这些属性只会影响到这个继承对象自身。而不是原始对象:

var o = {x: "donot change this balue"};
library_function(inherit(o));//防止对o的意外修改

2.查询和设置属性

(1)查询和设置

对象属性值可以通过点 . 和 方括号[] 运算符来查询或设置

var book = { 'author': 'Tom', 'main title': 'Hello JavaScript' };
var author = book.author;           //1.获取book的“author”属性值
var title = book["main title"];     //2.获取book的“main title”属性值
book.edition = 6;                   //3.给book创建一个“edition”属性
book["main title"] = "ECMAScript";  //4.修改"main title"属性值

ES3版本中,如果属性名是关键字必须通过方括号的形式访问。ES5版本放宽了要求,可以直接在点运算符后面直接使用保留字。

(2)关联数组对象

上面提到可以通过object["property"]操作对象属性,这种语法看起来更像数组,只是这个数组元素是通过字符串索引而不是数字索引,这类数组被称为关联数组。JavaScript对象都是关联数组,通过[]访问对象属性时,在程序运行时可以创建或修改它们,更有灵活性。但是当通过 . 访问时不可进行修改。

var addr = "";
for (i = 0; i < 4; i++) {
    addr += customer["address" + i] + '\n';
};

当使用for/in循环遍历关联数组时,就可以体会到for/in强大之处。下面的例子 就是for/in计算portfolio的总计算值

function getvalue(protfolio) {
    var total = 0.0;
    for (stock in protfolio) { //遍历protfolio中的每只股票
        var shares = protfolio[stock]; //得到每只股票的份额
        var price = getquote(stock); //查找股票的价格
        total += shares * price; //将结果累加到total中
    }
    return total;
}

(3)继承

JavaScript对象的属性分两种,一种是自己定义的,被称为“自有属性”。也有一些属性是从原型对象继承过来的。对象属性的多继承关系构成了原型链。

假设要查询对象o的属性x,如果o中不存在x,那么将会继续在o的原型对象中查询属性x。如果原型对象中也没有x,但这个原型对象也有原型,那么继续在这个原型对象的原型上执行查找。直到找到x或者查找到一个原型是null为止。可以看到,对象的原型属性构成了一个“链”,通过这个“链”可以实现属性的继承。

var o = {}              //o从Object.prototype继承对象的方法
o.x = 1;                //给o定义一个属性x
var p = inherit(o);     //p继承o和Object.prototype
p.y = 2;                //给p定义一个属性y
var q = inherit(p);     //q继承p、o和Object.prototype
q.z = 3;                //给q定义一个属性z
var s = q.toString();   //toString()继承Object.prototype
q.x + q.y               //=>3:x和y分别继承自o和p    
q.x = 5
o.x                     //1 继承属性不影响原属性

(4)属性访问错误

对象属性在赋值前会先检查原型链,以此判断是否允许赋值操作。例如,如果对象o继承自一个只读属性x,那么对x属性赋值是不允许的。如果允许属性赋值,也只是在原始对象上创建或对已有的属性赋值,而不会修改原型链。

JavaScript中,一般只有在查询属性的时候才能体会到继承的存在,而设置属性和继承无关。通过这个特性可以有选择的覆盖继承的属性。

属性访问并不返回或设置一个值。

查询一个不存在的属性不会报错,如果对象o自身的属性或继承的属性中均未找到属性x,属性的访问表达式o.x返回undefined。

但是,如果对象不存在,那么试图查询这个不存在的对象就会报错,null和undefined值都没有属性,因此查询这些值会报错:

//抛出一个类型错误异常,undefined没有length属性
var len = book.subtitle.length;

除非确定book和subtitle都是(或在行为上)对象,否则不能写成book.subtitle.length,否则会报错,下面提供了两种避免出错的方法:

var lerr = undefined;
if (book){
    if(book.subtitle) len = book.subtitle.length;
}
//一种更简练的常用的方法,获取subtitle的length属性undefined
var len = book&&book.subtitle && book.subtitle.length;

当然给null和undefined设置属性也会报类型错误。给其它值设置属性也不总是成功。一些属性是只读的,不能重新赋值,有一些对象不允许新增属性。但这些属性设置失败时不会 报错。

//内置构造函数的原型是只读的
Object.prototype = o;//赋值失败,但没报错,也没有修改 

这个bug在ECMAScript5的严格模式已经修复。严格模式下,失败的属性设置操作都会抛出类型错错误的异常。

尽管属性的赋值成功或失败规律看起来很简单,但是要描述清楚并不容易,在下面的场景中,给对象o设置属性p会失败:

  1. o的属性p是只读的,不能给只读属性从新赋值(defineProperty()方法中只有一个例外, 可以对可配置的只读属性从新赋值)。
  2. o的属性p是继承属性,且它是只读的:不能通过同名自有属性覆盖只读的继承属性。
  3. o中不存在自有属性p:o没有使用setter方法继承属性p.并且o的可扩展性(extensible attrubute)是false(6.8.3).如果o中不存在p,而且没有setter方法可调用,则p一定会添加到o中。但如果o不是可扩展的,那么o不能定义新属性。

3.删除属性

delete运算符可以删除对象的属性。它的操作数应当是一个属性访问表达式,delete只是断开属性和宿主对象的联系,而不会操作属性中的属性。

delete book.author; //book不再有属性author
delete book.["main title"] //book不再有属性"main title"
console.log("author" in book);  // => false
console.log("main title" in book); // => false

注:a={p:{x:1}}; b=a.p;delete a.p执行这段代码后,b.x的值仍然是1.由于已经删除的属性引用依然存在,因此在javascript的某些实现中,可能因为这种不严谨而造成内存泄漏,所以在销毁对象的时候,要遍历属性中的属性,依次删除。

delete运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它, 而且这会影响到所有继承这个原型的对象)。

当delete表达式删除成功或没有任何副作用(比如删除不存在的属性)时,它返回true。如果delete后不是一个属性访问表达式,delete同样返回true:

o = {x:1};           //o有一个属性x,并继承属性toString
delete o.x;          //删除x,返回true
delete o.x;          //什么都没做,(x已经不存在),返回true
delete o.toString(); //什么也没有做(toString是继承来的),返回true
delete o.toString(); //返回true
delete 1;            //无意义

delete不能删除那些可配置性为false的属性(尽管可以删除不可扩展对象的可配置属性)。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建全局对象的属性。在严格模式中,删除一个不可配置的属性也会报一个类型错误。在非严格模式中,这些情况下的delete操作也会返回false

delete Object.prototype; //不能删除,属性是不可配置的
var x = 1; //声明一个全局变量
console.log(delete this.x); //不能删除整个属性
function f() {} // 声明一个全局函数
console.log(delete this.f); //也不能删除全局函数

当在非严格模式中删除全局对象的可配置值属性时,可以省略对全局对象的引用,直接在delete操作符后跟随要删除的属性名即可:

this.x = 1; //创建一个可配置的全局属性(没有用var)
delete x; //将它删除

然而在严格模式下,delete后跟随一个非法的操作数(比如x),则会抱一个语法错误,因此必须显示指定对象及其属性:

delete x; //在严格模式下报语法错误 
delete this.x; //正常工作

4.检测属性

javascript对象可以看做是属性的集合,我们经常会检测集合中成员的所属关系--判断某个属性是否存在于某个对象中。可以通过in运算符、hasOwnProperty()和propertyIsEnumerable()方法来完成这个工作,甚至仅通过属性查询也做到这一点。

in运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true:

var o = {x: 1}
"x" in o; //=>true
"y" in o; //=>false y不是o的属性
"toString" in o;//=>true o继承toString属性

对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性,对于继承属性它将返回false:

var o = {x: 1};
o.hasOwnProperty("x"); //=>true o中有一个自有属性x
o.hasOwnProperty("y"); //=>false 
o.hasOwnProperty("toString"); //false toString是继承属性

propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到是自有属性且这个属性的可枚举性(Enumerable attrubute)为true时才返回true。某些内置属性是不可枚举的。通常javascript代码创建的属性都是可枚举的,除非在ECMAScript5中使用一个特殊的方法来改变属性的可枚举性

var o = inherit({y: 2});
o.x = 1;
o.propertyIsEnumerable("x"); //true:o是一个可枚举的自有属性
o.propertyIsEnumerable("y");//false y是继承来的
Object.prototype.propertyIsEnumerable("toString");//false 不可枚举

除了使用in运算符之外,另一种更简便的方法是使用"!=="判断一个属性是否是undefined:

var o = {x: 1}
o.x !== undefined; //true o有属性x
o.y !== undefined; //fakse: o没有属性y
o.toString() !== undefined; //true:o继承了toString属性

然而有一种场景只能用in运算符不能用上述的属性访问方式。in可以区分不存在的属性和存在值为undefined的属性。例如下面的代码:

var o = {
        x: undefined
    } //
o.x !== undefined; //属性存在,但值为undefined
o.y !== undefined; //属性不存在
"x" in o; //true
"y" in o; //false
delete o.x; //删除了属性x
"x" in o //false  属性不存在

注意,上述代码中使用的是"!==",而不是"!="。"!=="可以区分undefined和null.有时则不必做这种区分:

//如果o 中有属性x,且x的值不是null undefined ,o.x乘以2
if (o.x = null) o.x *= 2;
//如果o中还有属性x, 且x的值不能转换false, o.x乘以2
//如果x是undefined、null、false、""、0、NaN、则它保持不变
if (o.x) o.x *= 2;

5.枚举属性

除了检测对象属性是否存在,我们还会经常遍历对象的属性。通常使用for/in循环遍历,ECMAScript5提供了两个更好的替代方案。

for/in循环在循环体中遍历所有可枚举的属性(包括自有属性和继承属性),把属性名称赋值给循环变量。

var o = {x: 1,y: 2,z: 3};          //三个可枚举的属性
o.propertyIsEnumerable("toString");//=>false,不可枚举
for(p in o)                        //遍历属性
console.log(p);                    //属性x y z 不会输出toString

 有很多实用工具库给Object.prototype添加了新的方法或属性,这些方法和属性可以被对象继承并使用。然后在ECMAScript5标准之前,这些新添加的方法是不能定义为不可枚举的,因此他们都可以在for/in循环中枚举出来。为了避免这种情况,需要过滤for/in循环返回的属性,下面两种方式是最常见的:

for(p in o){
    if(!o.hasOwnProperty(p)) continue; //跳过继承的属性
}
for(p in o){
    if (typeof o[p]==="function") continue; //跳过方法
}

下面的例子定义了一些有用的工具函数来操控对象的属性,这些函数用到了for/in循环。实际上extend()函数经常出现在javascript实用工具库里

/*
 *把p中可枚举的属性复制到o中,并返回o
 *如果o和p中含有同名属性,则覆盖o中的属性
 *这个函数并不出来getter和setter以及复制属性
 */


function extend(o, p) {
        for (prop in p) { //遍历p中所有的属性
            o[prop] = p[prop]; //将遍历属性添加至o中
        }
        return o;
    }
    /*将p中可枚举的属性复制至o中,并返回o
     * 如果o和p有同名属性,o中的属性将不受影响
     * 这个函数并不出来getter和setter以及复制属性
     */


function merge(o, p) {
    for (prop in p) { //遍历p中所有的元素
        if (o.hasOwnProperty[prop]) continue; //过滤掉已在o中存在的属性
        o[prop] = p[prop]; //将属性添加至o中
    }
    return o;
}


/*
 * 如果o中的属性在p中没有同名属性,则从o中删除这个属性
 * 返回o
 */
function restrict(o, p) {
    for (prop in o) { //遍历o的所有属性
        if (!(prop in p)) delete o[prop]; //如果在p中不存在,则删除之
    }
    return o;
}


/*
 * 如果o中的属性在p中存在属性,则从o中删除这个属性
 * 返回o
 */
function subtarck(o, p) {
    for (prop in p) { //遍历p中所有的属性
        delete o[prop]; //从o中删除(删除一个不存在的属性一般不报错)
    }
    return o;
}


/*
 * 返回一个新对象,这个对象同事拥有o的属性和p的属性
 * 如果o和p中有同名属性,使用p中的属性
 */
function union(o, p) {
    return extend(extend({}, o), p);
}


/*
 * 返回一个新对象,这个对象同事拥有o的属性和p中出现的属性
 * 很像求o和p的交集,但p中的属性值被忽略
 */
function intersection(o, p) {
    return restrict(extend({}, o), p);
}


/*
 * 返回一个数组,这个数组包含的是o中可枚举的自由属性的名字
 */
function keys(o) {
    if (typeof o !== "object") throw TypeError(); //参数必须是对象
    var result = []; //将要返回的对象
    for (var prop in o) { //遍历所有可枚举的属性
        if (o.hasOwnProperty(prop)) //判断是否自有属性
            result.push(prop); //将属性名添加至数组中
    }
    return result; //返回这个数组
}

除了for/in循环外,ECMAScript5定义了两个用以枚举属性名称的函数。一个是Object.keys(),它返回一个数组,这个数组由对象中可枚举的自由属性的名称组成,它的工作原理和上述例子最后的函数keys()相似。

ECMAScript5中的第二个枚举属性的函数是Object.getOwnPropertyNames(),它和keys()类似,只是它返回的对象的所有自有属性的名称,而不仅仅是可枚举的属性。在ECMAScript3中是无法实现类似的函数的,因为ECMAScript3没有提供任何反复来获取对象的不可枚举属性。

6.属性getter和setter

由getter和setter定义的属性称作“存取器属性”(accessor property),它不同于“数据属性”(data property),数据属性只有一个简单的值。

和数据属性不同,存取器属性具有不可写(writable attribute)。如果属性同时具有gettter和settter方法,那么它是一个读/写属性。如果它只有getter方法,它是一个可读属性。如果它只有setter方法,那么它是一个只写属性(数据属性中有一些例外),读取只写属性总是返回undefined。

定义存取器属性最简单的方法是使用对象直接量语法的一种扩展写法:

var o = { 
    //普通数据属性
    data_prop: value,


    //存取器属性都是成对定义的函数
    get accessor_prop() { /*这里是函数体*/ },
    set accessor_prop(value) { /*这里是函数体*/ }
};

存取器属性定义一个或两个和函数同名的函数,这函数定义没有使用function关键字,而是使用get和(或)set,函数体的结束和下一个方法或数据属性之间有逗号分隔。

var p = {
    //x和y是普通的可读写数据属性
    x: 1.0,
    y: 1.0,
    //r是可读写的存取器属性,它带有getter和setter。
    //函数体结束后不要忘记带上逗号
    get r() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    },
    set r(newvalue) {
        var oldvalue = Math.sqrt(this.x * this.x + this.y * this.y);
        var ratio = newvalue / oldvalue;
        this.x *= ratio;
        this.y *= ratio;
    },
    //theta是只读存取器属性,它只有getter方法
    get theta() {
        return Math.atan2(this.y, this.x);
    }
};

和数据属性一样,存取器属性是可以继承的,因此上述代码中的对象p可以当成令一个“点”的原型。可以给新对象定义它的x和y属性,但r和theta属性是继承来的:

var q = inherit(p); //创建一个继承getter和setter的新对象
q.x = 1, q.y = 1; //给q添加两个属性
console.log(q.r) //可以使用存储器的存取属性
console.log(q.theta);

这段代码使用存取器属性定义API,API提供了表示同一组数据的两种方法(笛卡尔坐标系和极坐标西表示法)。还有很多场景用到存取器属性,比如智能检测属性的写入值以及在每次属性读取时返回不同值:

//这个对象产生严格的自增序列号
var serialnum = {
    //这个属性包含下一个序列号
    //$符号暗示这个属性是一个私有属性
    $n: 0,


    //返回当前的值,然后自增
    get next() {
        return this.$n++;
    },


    //返回当前新的值,大只有当它比当前值大时才设置成功
    set next(n) {
        if (n >= this.$n) this.$n = n;
        else throw "序列号的值不能比当前值小";
    }
};
var q = inherit(serialnum);
console.log(q.next);
q.next = 3;
console.log(q.next);
这个例子使用getter方法实现一种“神奇”的属性



 //这个对象表示有一个可以返回随机数的存取器属性
 //例如,"random.octet"产生一个随机数
 //每次产生的随机数都在0-255之间
var random = {
    get octet() {
            return Math.floor(Math.random() * 256);
        },
        get uint16() {
            return Math.floor(Math.random() * 65536);
        },
        get int16() {
            return Math.floor(Math.random() * 65536) - 32768;
        }
}

7.属性的特征

ECMAScript 3版本下对象的属性都是否可写、可配置和可枚举的,但是到ECMAScript 5版本下是属性是可以通过一些API来标识是否为可写、可配置和可枚举的。这API也就是所谓的属性的特性。

  • 普通数据属性的4个特性:value(值)、writable(可写性)、enumerable(可枚举性)、configurable(可配置性)。
  • 存储器属性的4个特性:get(读取)、set(写入)、enumerable(可枚举性)、configurable(可配置性)。

ECMAScript 5中定义了一个Object.getOwnPropertyDescriptor()方法用来查询对象特定属性的特性,返回一个“属性描述符”对象,该对象就代表对象属性的4个特性。

//通过调用Object.getOwnPropertyDescriptor()获得某个对象特定的属性描述符


//返回{value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor({ x: 1 }, "x");
console.log(Object.getOwnPropertyDescriptor({ x: 1 }, "x"))


//查询上例子中的randam对象的octet属性
//返回 {get: /*function octet(){...*/ , set: undefined, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(random, "octet");
console.log(Object.getOwnPropertyDescriptor(random, "octet"));


//对于继承属性和不存在的属性,返回undefined
Object.getOwnPropertyDescriptor({}, "x"); //undefined 没有这个属性
console.log(Object.getOwnPropertyDescriptor({}, "x"));


Object.getOwnPropertyDescriptor({}, "toString"); //undefined 继承属性 
console.log(Object.getOwnPropertyDescriptor({}, "toString"));

Object.getOwnPropertyDescriptor()只能得到自有属性的描述符。要想获得继承属性的特性,需要遍历原型链。

要想设置属性的特性,或者想让新建的属性具有某种特性,则需要调用Object.defineProperty()方法,第一个参数是要修改的对象;第二个参数是要修改的属性;第三个是属性描述符对象。返回值为修改后的对象副本。

var o = {}; //空对象
//添加一个不可枚举的数据属性x,并赋值1
Object.defineProperty(o,"x",{value:1,writable:true,enumerable:false,configurable:true});
// {value: 1, writable: true, enumerable: false, configurable: true}
Object.getOwnPropertyDescriptor(o,"x");


//属性是存在的,但不可枚举
o.x; //=> 1
Object.keys(o) //=> [];


//现在对属性x修改,让它只变为只读
Object.defineProperty(o,"x",{writable:false});


//试图改变这个属性的值
o.x=2; //操作失败但不报错,严格模式中会抛出类型错误的异常
o.x //=> 1


//属性依然是可配置的,因此可以通过这样的方式进行修改:
Object.defineProperty(o,"x",{value:2});
o.x //=>2


//现在讲x从数据属性修改为存取器属性
Object.defineProperty(o,"x",{get: function(){return o;} });
o.x //=>o

该方法同样不能设置继承属性的特性。如果需要同时修改多个自有属性的特性可以使用Object.defineProperties()方法。第一个参数是要修改的对象;第二参数是一个映射表对象,它包含属性名称和对应属性的描述符对象。

 var p = Object.defineProperties({},{
    x:{value:1,writable:true,enumerable:true,configurable:true},
    y:{value:1,writable:true,enumerable:true,configurable:true},
    z:{
        get:function(){
            return Math.sqrt(this.x*this.x + this.y*this.y)},enumerable:true,configurable:true
    }
});
console.log(p) //=>Object {x: 1, y: 1, z: 1.4142135623730951}

下面是完整的规则,任何对Object.defineProperty()或Object.defineProperties()违反规则的使用都会抛出类型错误的异常。

  1. 如果对象是不可扩展的,则可以编辑已有的自有属性,但不能给它添加新属性
  2. 如果属性是不可配置的,则不能修改它的可配置性和和可枚举性
  3. 如果存取器属性是不可配置的,则不能修改其getter和setter方法,也不能将它转换为数据属性
  4. 如果数据属性是不可配置的,则不能将它转换为存取器属性
  5. 如果数据属性是不可配置的,则不能将它的可写性从false修改为true,但可以从true修改为false。
  6. 如果数据属性是不可配置且不可写的,则不能修改它的值。然后可配置但不可写属性值是可以修改的(实际上将它先标记为可写的,然后修改它的值最后转换为不可写的)

使用Object.defineProperty()和Object.defineProperties()对属性的所有特性进行复制。

/*
 *复制属性的特性
 *Object.prototype添加一个不可枚举的extend()方法
 * 这个方法继承自调用它的对象,将作为参数传入的对象的属性一一复制
 * 除了值之外,也复制属性的所有特性,除非在目标对象中有同名的属性
 * 参数对象的所有自有对象(包括不可枚举的属性)也会一一复制
 * */
Object.defineProperty(Object.prototype,
    "extend", //定义Object.prototype.extend
    {
        writable: true,
        enumerable: false, //将其定义为不可枚举的
        configurable: true,
        value: function(o) { //值就是这个函数
            //得到所有的自由属性,包括不可枚举属性
            var names = Object.getOwnPropertyNames(o);
            //遍历他们
            for (var i = 0; i < names.length; i++) {
                //如果属性已经存在,则跳过
                if (names[i] in this) continue;
                //获得o中的属性描述符
                var desc = Object.getOwnPropertyDescriptor(o, names[i]);
                //用它给this创建一个属性
                Object.defineProperty(this, names[i], desc);
            }
        }
    }
);

8.对象的三个属性

每一个对象都有与之相关的原型(protype)、类(class)、和可扩展性(extensible attribute)。

(1)原型属性

对象的原型是用来继承属性的,这个属性非常重要,以至于经常把“o的原型属性”直接叫做“o的原型”。

原型属性是在对象创建之初就设置好的。前面已对原型做过介绍,但这里还是要补充补充。

  • 通过对象直接量创建的对象使用Object.prototype作为原型;
  • 通过new关键字创建的对象使用构造函数的prototype作为原型;
  • 通过Object.create()创建的对象使用第一个参数作为原型。

在ES5版本中,将对象传入Object.getPrototypeOf()方法可以查询它的原型对象。

要检测一个对象是否是另一个对象的原型(或处于原型链中),请使用isPrototypeOf()方法。例如通过p.isPrototypeOf(o)用来检测p是否为o的原型。

var p = {x: 1};                     //一个原型对象
var o = Object.create(p);           //使用这个原型创建一个对象
p.isPrototypeOf(o);                 //=>true o继承自p
Object.prototype.isPrototypeOf(p);  //=>true p继承自Object.prototype

(2)类属性

对象的类属性是一个字符串,用来表示对象的类型信息。但是JS中没有提供直接查询方法,只能用一种间接的方法查询,可以调用对象的toString()方法,然后提取返回字符串的第8个字符至倒数第二个位置之间的字符。如果对象继承的toString()方法重写了,这时必须间接通过Function.call()方法调用。

function classOf(o) {
    if (o === null) return "Null";
    if (o === undefined) return "Undefined";
    return Object.prototype.toString.call(o).slice(8, -1);
}

classof()可以接收任何类型的参数,并且该函数包含了对null和undefined的特殊处理。

classOf(null)         //=>"Null"
classOf(1)            //=>"Number"
classOf("")           //=>"String"
classOf(false)        //=>"blooean"
classOf({})           //=>"Object"
classOf([])           //=>"Array"
classOf(/./)          //=>"RegExp"
classOf(new Date())   //=>"Date"
classOf(window)       //=> "window"(这是客户端宿主对象)
function f() {}       //定义一个自定义构造函数
classOf(new f())      //=> "Object"

(3)可扩展性

对象的可扩展行用来表示是否可以给对象添加新属性。ECMAScript 5版本中,所有自定义对象、内置对象和宿主对象默认支持可扩展性。下面介绍几个检测和设置对象可扩展性的方法以及它们之间的区别。

Object.preventExtensions()方法能将传入对象设置为不可扩展的。需要注意的两点是:

  1. 一旦对象转为不可扩展的,就无法再将其转换成可扩展的;
  2. 如果给一个不可扩展的对象的原型添加属性,这个不可扩展的对象同样会继承这些新属性。Object.isExtensible()方法可以检测传入对象的可扩展性。

Object.seal()方法能将传入对象设置为不可扩展的,并且将对象所有自有属性都设置为不可配置的。也就是说不能给这个对象添加新属性,而且也不能删除或配置已有属性,但是它已有的可写属性依然可以配置。对于已经密封的对象同样不能解封,可以使用Object.isSealed()方法检测对象是否封闭。

Object.freeze()将更严格地锁定对象----"冻结"(frozen)。除了将对象设置为不可扩展的和将其属性设置为不可配置的之外,还可以将它的自由所有数据属性设置为只读(如果对象的存取器属性具有setter方法,存取器属性将不受影响,仍然可以通过给属性赋值调用他们)。使用Object.isFrozen()来检测是否冻结对象。

Object.preventExtensions()、Object.seal()和Object.freeze()三个方法都返回传入的对象。

//创建一个封闭对象,包括一个冻结的原型和一个不可枚举的属性
var o = Object.seal(Object.create(Object.freeze({x: 1}), {
    y: {value: 2,writable: true}
}));

9.序列化对象

对象序列化(serialization)是将对象的状态转换为字符串,也可以将字符串还原为对象。ECMAScript5提供了内置函数JSON.stringify()和JSON.parse()用来序列化和还原javascript对象。这些方法都使用JSON作为数据交换格式,JSON的全称是"Jvascript Object Notation" ——javascript对象表示法,它的语法和javascript对象与数组直接量的语法非常接近:

o = {x: 1,y: {z: [false, null, ""]}}; //定义一个测试对象
s = JSON.stringify(o); //s是'{"x":1,"y":{"z":[false,null,""]}}'
p = JSON.parse(s)      //p是o的深拷贝

10.对象方法

上文描述过,所有的javascript对象都是从Object.prototype继承属性(除了那些不通过原型显式创建的对象)。这些继承属性主要是方法,因为javascript程序员普遍对继承方法更感兴趣。我们已经讨论过hasOwnProprtty()、propertyIsRnumerable()、isPrototypeOf()这三个方法,以及在Object构造函数里定义的静态函数Object.create()和Object.getPrototypeOf()的等。本节将对定义在Object.prototype里的对象方法展开讲解。

(1)toString()方法

toString没有参数,它将返回一个表示调用这个方法对象值的字符串。在需要将对象转换为字符串的时候,javascript会调用这个这个方法。比如,使用“+”号连接一个字符串和一个对象时或者在希望使用字符串的方法中使用了对象时都会调用toString()

默认的toString()方法在返回值带有的信息量很小,例如下面的代码计算结果为 [object Object]

var s ={x:1,y:1}.toString(); //=>[object Object]

由于默认的toString()方法并不会输出很多有用的信息,因此很多类有带有自定义的toString()。例如,将数组转换为字符串时,结果是一个元素列表,只是每个元素都转换成了字符串。再比如,当函数转换为字符串时,得到函数的源代码。第三部分有关于toString()详细文档说明。比Array.toString(),Date.toString以及function.toString().

(2)toLocaleString()方法

除了基本的toString()方法之外,对象都包含toLocaleString()方法,这个方法返回一个表示这个对象本地化的字符串。

Object中默认的toLocaleString()方法并不做任何本地化的自身的在,它仅调用toString方法返回对应值。Date和Number类对toLocaleString()方法做了定制,它可以对数字、日期和时间做本地化转换。Array类的toLocaleString()方法和toString方法很像,唯一不同的是每个数组元素会调用toLocaleString()方法转换为字符串,而不是调用各自的toString()方法。

(3)toJSON()方法

Object.prototype实际上没有定义toJSON()方法,但对于要执行序列化的对象来说,JSON.stringify()方法会调用到toJSON()方法。如果在待序列化的对象中存在这个方法则调用它,返回值即是序列化的结果,而不是原始的对象。具体示例参加Date.toJSON()。

(4)valueOf()方法

valueOf()和toString()方法非常类似,但往往javascript需要将对象转换为某种原始值而非字符串的时候才调用它,尤其是转换为数字的时候。如果在需要使用原始值的上下文中使用了对象,javascript就会自动调用这个对象。默认的valueOf()方法不足为奇,但有些内置自定义了valueOf()方法(比如Date.valueOf()。

五、数组

1.创建数组

数组字面量

var arr = [1,2,3];

使用构造函数Array(创建的数组是稀疏数组)

var arr = new Array(10);

2.数组元素的读和写

  1. 数组是属于特殊对象。
  2. 如果使用的属性是数组的索引,那么数组将会根据需要更新其length。
  3. 如果使用非数字或者负数来索引数组,那么数值会转换为非数组索引,当做常规对象的属性。
  4. 数组的索引属于对象属性名的一种特殊类型。
a[-1.23] = true;  //数组拥有"-1.23"这个属性
a["1000"] = 0;    //数组的索引
a[1.0000] = 0;    //等同于a[1]

3.稀疏数组

  1. 稀疏数组就是索引从0开始不连续的数组。
  2. 可以通过new Array(n)来创建稀疏数组。
  3. 可以通过delete操作符来产生稀疏数组。
var a1 = [undefined];      //数组是[undefined]
var a2 = new Array(3);     //该数组没有元素
console.log(0 in a1);      // => true : 在索引0处有一个元素 undefined
console.log(0 in a2);      // => false : 在索引0处没有元素

所以值不存在和值为undefined是不一样的。

重点:如果对索引数组进行遍历,不存在的索引不会被遍历到。

如果用图来表示这些东西的关系,那么"",0,false在一个圈圈里,undefined在另外一个圈圈里,而null则在两个圈圈的交叉部分。https://blog.csdn.net/qq_33199019/article/details/122672677

4.数组长度

  1. 数组有length属性,代表数组的元素个数,稀疏的length大于数组元素的个数。
  2. 当设置length属性为一个小于当前长度的非负整数n时,当前数组中那些索引大于等于n的元素将从中删除
    var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    var length = arr.length;  //10
    arr.length = 5;           //数组变为[0,1,2,3,4]
  3. 可以用Object.defineProperty()让数组的length属性变为只读
var arr = [1,2,3];
Object.defineProperty(arr, 'length', {
		writable: false
	}
);		

5.数组元素的增删

增加元素:push,unshift,返回的数组的长度

push(): 向数组末尾添加一个或者多个元素,并返回新的长度。

unshift() :向数组最前方添加一个或者多个元素,并且会返回新的数组长度。

var arr = [];
arr.push(10);   //返回1(数组长度) 
arr.unshift(20);   //返回2 (数组长度)

删除元素:pop,shift,返回的被删除元素

pop(): 删除数组的最后一个元素,把数组的长度减1,并且返回它被删除元素的值,如果数组变为空,则该方法不改变数组,返回undefine值。

shift() : shift()方法和unshift()方法相反。shift()把数组的第一个元素从其中删除,并返回被删除的值。如果数组是空的,将不进行任何操作,返回undefined。

var arr = [1,10,100];
arr.pop();   //返回100        
arr.shift();   //返回1

当然,数组也是对象,也可以使用delete操作符对数组元素进行删除,但是和上面两个方法不同的是,删除后数组长度不变,相当于变成稀疏数组。

splice方法也能够删除元素。

6.多维数组

let arr = new Array();
        for (let i = 0; i < 2; i++) {
            arr[i] = new Array();
            for (let j = 0; J < 3; j++) {
                arr[i][j] = i * j;
            }
        }

7.数组方法

Array.join():不改变原数组

将数组中所有元素根据分隔符连接起来,返回字符串形式(默认为 , )

var arr = [1,2,3]
arr.join(); //"1,2,3"
arr.join("-"); //"1-2-3"

Array.reverse() :会改变原数组

将数组元素反转

var arr = [1,2,3];
arr.reverse(); //[3,2,1]


var arr = ['a','b','c'];
arr.reverse(); //["c","b","a"]

Array.sort():会改变原数组

当参数为空的时候,按照字母表顺序来排序

当参数为函数的时候,返回大于0是升序,返回小于是降序

var arr1 = ["hello", "apple", "banana"];
arr.sort();                                 //  ["apple","banana","hello"]


var arr2 = [111,4,22,12345,222];
arr2.sort(function(a,b){
    return a - b;
});                                         //  [4,22,111,222,12345]

Array.concat():不会改变原数组

创建并返回一个新数组,它的元素包括原始数组的元素和concat()的每个参数

var arr = [1,2,3];
arr.concat(4,5);  //[1,2,3,4,5]
arr.concat(6,7); //[1,2,3, 6, 7]

Array.slice():不会改变原数组

参数:起始索引,结束索引(如果索引为负数,那就加上数组的length)

返回子数组

var arr = [1,2,3,4,5];
arr.slice(); //[1,2,3,4,5]
arr.slice(1,3); //[2,3]
arr.slice(1,-1); //[2,3]

Array.splice():会改变原数组

第一个参数表示插入或者删除元素的索引

第二个参数表示从指定索引删除多少个元素(未说明默认从索引开始全部删除)

后续参数表示从指定索引插入传入的参数

var arr = [0,1,2,3,4,5,6,7,8,9];
arr.splice(4);                      //  返回[4,5,6,7,8,9],arr的元素[0,1,2,3]
arr.splice(1,2);                    //  返回[1,2],arr的元素[0,3]
arr.splice(1,1);                    //  返回[3],arr的元素[0]


var arr2 = [0,1,2,3,4];
arr2.splice(1,2,"a","b");           //  返回[1,2],arr的元素[0,"a","b",3,4]
arr2.splice(3,0,"c");               //  返回[],arr的元素[0,"a","b","c",3,4]

Array.toString()方法

将每个元素转换为字符串,(如有必要将调用每个元素的toString方法)并且输出用逗号分隔的字符串列表。

[1,2,3].toString(); //1,2,3
[1,[2,3],4].toString(); //1,2,3,4

8.ES5数组方法

forEach()

从头到尾遍历,并且为每个元素调用指定函数

参数:函数(数组元素,数组元素索引,数组本身)

forEach方法无法在所有元素传递给调用函数之前提前终止。

var data = [1, 2, 3, 4, 5];
//计算数组元素的和值
var sum = 0;
data.forEach(function(value){
     sum += value;
});
console.log(sum); //15
//每个数组的值自加1
data.forEach(function(v, i, a) {
     a[i] = v + 1;
});
console.log(data);//[2, 3, 4, 5, 6]

map()

map()函数和forEac和()方法一致,但map()有返回值,如果数组是稀疏数组,那么返回的数组也是稀疏数组

参数:函数(数组元素,数组元素索引,数组本身)

传入的参数函数必须要有返回值

返回的新数组和原数组的长度相同

var arr = [1, 2, 3, 4, 5];
arr.map(function (value) {
    return value * value;
});
console.log(arr.join());  //  "1,4,9,16,25"

filter()

参数:函数(数组元素,数组元素索引,数组本身)

传入的参数函数返回true或者false,将返回的true的值过滤出来返回一个新数组

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
//  返回数组中偶数子集
var flag = arr.filter(function(v, i) {
     return i%2 == 0;
});
console.log(flag); //[1,3,5,7,9]

every()和some()

every()

参数:函数(数组元素,数组元素索引,数组本身)

当且仅当所有元素传入函数并且能够返回true才会返回true,如果一旦有一个返回false,那就返回false

如果一旦某个元素返回false,那么就停止遍历后面的元素

some()

参数:函数(数组元素,数组元素索引,数组本身)

当有元素传入函数并且能够返回true就会返回true,如果全部元素传入函数都返回false,那就返回false

如果一旦某个元素返回true,那么就停止遍历后面的元素

var arr = [0,1,2,3,4,5,6,7,8,9];
arr.every(function(value){
    return value < 10;
});                                 //true
arr.every(function(value){
        return value % 2 == 0;
});                                 //false
arr.some(function(value){
    return value > 10;
});                                 //false
arr.some(function(value){
        return value % 2 == 0;
});                                 //true

reduce()和reduceRight()

第一个参数:函数,函数参数(默认值,第一个元素,第一个元素索引,数组本身)

第二个参数:传入第一个函数第一个参数的默认值

如果第二个参数没给,那默认值就是数组第一个元素

函数参数为(第一个元素,第二个元素,第二个元素索引,数组本身)

函数返回的值将作为下一个遍历的默认值

reduceRight()和reduce()的区别就是reduceRight()数组索引从高到低处理(倒着处理)。

//reduce()和reduceRight()
var arr = [1, 2, 3, 4, 5];
//  0是初始值,然后 (((((0+1)+2)+3)+4)+5)
var num = arr.reduce(function (a, b) {
     return a + b;
}, 0);
console.log(num);
var arr2 = [2, 3, 4];
//  pow(2,pow(3,4))
var num = arr2.reduceRight(function (a, b) {
    return Math.pow(a, b);
});
console.log(num);

indexOf()和lastIndex0f()

indexOf正向搜索给定的值,如果存在返回匹配到第一个元素的索引,否则返回-1。

lastIndexOf()反向搜索。

第一个参数为寻找的元素,第二个参数为开始搜索的索引位置,如果第二个参数为负数,则代表相对末尾的偏移量。例如-1指代为最后一个元素

var arr = [0, 1, 2, 1, 0];
console.log(arr.indexOf(1));     //  返回值为1,arr[1]是1
console.log(arr.lastIndexOf(1)); //  返回值为3,arr[3]是1
console.log(arr.indexOf(1, 2));   //  返回值为3,arr[3]是1

判断是否为数组

Array.isArray(arr);

Array.isArray([]);      //  true
Array.isArray({});      //  false

六、函数

1.函数定义

一是函数声明,实际上是声明了一个变量,该变量指向函数对象;

二是函数表达式,采用该方式函数的名称是可选的,通常没有函数名,但也可以有,例如:

var f = function fact(x) {
    if (x <= 1) {
        return 1;
    }
    else {
        return x * fact(x-1);
    }
}

此时,函数名称实际上是函数内部的一个局部变量。通常都不许要名称,除非像这样需要递归的时候。

使用函数表达式适合用来定义那些只会用到一次的函数。

函数声明并不是正真的语句,可以出现在全局或嵌套在其他函数中,但不能出现在if、循环、try…块中,但函数定义表达式可以出现在JS代码的任何地方。

function fname(value) {
            //函数体
            return value*value;
        }

嵌套函数

//嵌套函数
        function hypotenuse(a, b)  {
            function square(x) { return x*x }
            return Math.sqrt(square(a) + square(b));
        }

2.函数调用

四种方式调用JS函数

  • 作为函数
  • 作为方法
  • 作为构造函数
  • 通过他们的call()和apply()方法调用

函数调用

直接进行函数调用

 //函数调用
printprops({x:1});
var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5);
var probability = factorial(5)/factorial(13);

方法调用

其调用的上下文是对象,在作为方法调用时实际上传入了一个隐式的实参,即对象。函数体内可使用this引用该对象。

当方法不需要返回值的时候最好直接返回this,这样就可以进行“链式调用”风格的编程。例如jQuery。

this是关键字不是变量,不能给this进行赋值。

嵌套的函数不会从调用它的函数中继承this。如果嵌套的函数作为方法调用,this指向调用它的对象;如果作为函数调用,this不是全局对象(非严格模式)就是Undefined(严格模式)。因此,如果想要访问外部函数的this值,需将this值保存在一个变量里,这个变量和内部函数都在同一个作用域内。

间接调用

call和apply允许显示地指定调用所需的this值,使得任何函数都可以作为任何对象的方法来调用。前者是将参数挨个传入,后者是参数以一个数组“打包传入”,因此当传入一个函数的实参是数组,但形参却期待是单一个的,可以采用如下形式:

func.apply(this,arrOfArg);

3.函数的实参和形参

1. 未传入的实参其值实际上为undefined,通常用 || 来设置可选形参的默认值:

function getPropertyNames(o, a) {
    a = a || []; //需注意使用||a必须预先声明,否则报错;此处形参已有则无需声明
    for(var pro in o) {
        a.push(pro);
    }
    return a;
}

2.当传入的实参个数超过了形参,没有办法直接获得未命名值得引用,但函数体内,有一个类数组对象arguments指向实参对象,可以通过下标来访问传入的实参。

function f(x) {
    console.log(x);
    arguments[0] = null;
    console.log(x);
}
var a = 12;
f(12);
var b = [1,2,3];
f(b);

上面代码中不管是对于实参a还是b,第二条console.log都显示null。执行arguments[0] === x 也是返回true。因此,不管实参是引用类型还是原始类型的值,arguments和x指代同一值,修改其中一个会影响到另一个。而不是像书上说的传入普通数组就不会输出null。

3.arguments对象的属性:

//length
//callee:指代当前正在执行的函数,在匿名函数中通过callee来递归地调用自身比较有用:
var fact = function(x) {
    if (x <= 1) {
        return 1;
    }
    return x * arguments.callee(x-1);
};

caller:指代调用当前正在执行的函数的函数

4.*为了在传入实参时简省记住形参顺序等的麻烦,可以将实参作为名/值对儿的形式传入,多个实参作为一个对象。*

4.自定义函数属性

函数是一种特殊的对象,可以像普通变量那样被赋值、作为参数、作为返回值。也可以有属性,当函数需要一个“静态“变量来在调用时保持某个值不变,可以定义为函数的属性而不是定义一个全局变量:

function fact(n) {
    if (isFinite(n) && n>0 && n==Math.round(n)) {
        if(!(n in fact)) {
            fact[n] = n * fact(n-1); //保存了每次的计算结果
        }
        return fact[n];
    }
    else {
        return NaN;
    }
}
fact[1] = 1;
fact(10);
for(var v in fact) {
    console.log(v + ': ' + fact[v]);
}

5.作为命名空间的函数

在函数内部定义的变量都是局部变量,因此可以定义一个函数来作为命名空间,内部定义的变量不会污染到外部的全局变量。

6.闭包

1.  明白作用域链:每当进入一个执行环境(全局或函数),都会生成一个变量对象用来保存该环境中的变量,这个对象会被添加到作用域链的最前面。内部的函数其作用域链中会包含指向外部环境作用域链上的变量对象的引用。

因此当外部函数执行完返回后,由于内部函数还持有对外部函数的变量对象的引用,那么外部函数作用域链上的变量对象就不会被销毁,所以如果外部函数执行返回了内部函数,通过内部函数仍然是能够访问到外部函数定义的变量的。

因此通过闭包可以间接地在函数外部访问到其内部定义的私有变量。

例子:

var uniqueInteger = (function() {
    var counter = 0;
    return function() { 
        console.log(counter);
        counter++; 
    };
})();
uniqueInteger(); //0
uniqueInteger(); //1
uniqueInteger(); //2

counter是函数定义的局部变量,但这个匿名函数立即执行并返回了一个闭包,这个闭包的作用域链中的变量对象持有外部函数的变量,因此当返回了闭包并赋值给uniqueInteger后仍然能在外部访问到counter。由于uniqueInteger持有对闭包的引用,因此闭包的作用域链一直都不会被销毁。

2.  函数在每次调用时都会创建一个新的作用域链。如果每次都能想明白形成作用域链上的变量对象到底是什么,就不难理解闭包了:

两个互不打扰的计数器:

function counter() {
    var n = 0;
    return {
        count: function() { return n++;},
        reset: function() { n = 0; }
    };
}
var c = counter();
var d = counter();
c.count;    // => 0
d.count;    // => 0:互不干扰
c.reset;    
c.count;    // => 0:c被reset重置
d.count;    // => 1;d没有被重置

由于每次调用counter都生成了新的作用域链,因此c、d能够访问到各自不同的私有变量n。

3.   多个闭包可以共享包含它们的函数内部的私有变量。闭包可以通过外部函数返回或者作为外部对象的方法,这样就可以实现在外部也能调用闭包。

4.   一个函数内部的闭包是共享这个函数内定义的局部变量的,而不会说每个闭包都各自复制一份局部变量,注意每个闭包的作用域链上指向外部函数的变量对象都是同一个,即位于外部函数作用域链最前面的变量对象。

function constfuncs() {
    var funcs = [];
    for (var i = 0; i < 10; i++) {
        funcs[i] = function() {
            return i;
        };
    }
    return funcs;
}
var funcs = constfuncs();
console.log(funcs[5]()); //10

这里,funcs存储了10个闭包的数组,所有的闭包都共享这一个局部变量i,而经过一轮循环后i变成了10.

5.   每个函数都有自己的this,因此嵌套函数是访问不到外部函数this值的,除非显示地存储在一个变量里。arguments对象也和this类似,必须存储在变量中才能被嵌套的函数访问到。

7.函数的属性、方法和构造函数

1.   arguments.length是实际传入的实参的个数,而arguments.callee.length即函数的length属性表示期待传入的实参的个数即形参的个数。

2.   prototype属性,每个函数都有一个该属性,它指向原型对象。当函数作为构造函数时,新创建的对象会从原型对象上继承属性。

3.   call和apply可以使得某个函数看起来像一个对象的方法,可以修改这个函数的调用上下文为传入的对象。在函数提内通过this获得传入的对象的引用。

在ES5严格模式中,传入的第一个实参都会变为this的值,即便传入的是原始值或null、undefined。而在ES3和非严格模式中,传入的null、undefined会被全局对象替代,原始值则会被包装对象所替代。

4.   bind方法可以将函数绑定要某个对象,使得它像这个对象的方法,bind返回一个新的函数,而不像call、apply那样修改作用域后立即调用。

实现bind方法:

function bind(f, o) {
    if (f.bind) {
        return f.bind(o);
    }
    return function() {
        return f.apply(o, arguments);
    }
}
function func(y) {
    return this.x + y;
}
var o = {
    x: 1
};
var resfunc = bind(func, o);
var res = resfunc(2); //3

传入bind的除第一个实参以外的其他参数会被绑定到this,该函数式编程技术被称为“currying”。即f.bind(o, …)可以传入一些参数按顺序作为f的参数,且传入的参数会作为o(即this)的属性。此外,f.bind(o,…)返回的函数在调用的时候还可以再传入实参,按照形参出现的顺序一一对应后面剩下的形参。

例如:

function f(y, z) {
    return this.x + y + z;
}
var g = f.bind({x: 1}, 2); //x==1, y==2
g(3); // z==3,返回6

5.   toString方法会返回函数的源代码。而Object.prototype.toString.apply(f)则返回”[Object Function]“。因为不同类型值的toString方法被重写了,因此检测类型的时候要用原型上的方法。

8.函数式编程

使用数组的map、reduce等方法可以进行函数式编程。

高阶函数

即接收一个或多个函数作为参数,并返回一个新的函数。是操作函数的函数。

例如 计算和的平法:

function compose(f, g) {
    return function() {
        return f.call(this, g.apply(this, arguments));
    }
}


var square = function(x) { return x * x; };
var sum = function(x, y) { return x + y; };
var squareOfSum = compose(square, sum);
squareOfSum(2, 3); //25

该函数接收两个函数f,g,并返回一个新的函数h,在h中进行f(g)的计算。h接收的参数传递给g,g的结果作为参数传递给f。g、f使用的this和h的相同。

不完全函数

  不完全函数编程是一种函数编程技巧,即把一次完整的函数调用拆成多次函数调用,每次传入的参数都是完成参数的一部分,每次才分开的函数就叫做不完全函数。每次函数调用就叫做不完全调用。

特点: 每次调用都返回一个函数,知道调用最终位置。

//实现一个工具函数将类数组对象(或对象)转换为真正的数组
        //在后面的示例代码中用到了这个方法将arguments对象转换为真正的数组
        function array(a, n) { return Array.prototype.slice.call(a, n || 0); }
        //这个函数的实参传递至左侧
        function partialLeft(f /*,...*/) {
            var args = arguments; //保存外部的实参数组
            return function () {   //并返回这个函数
                var a = array(args, 1); //开始处理外部的第1个args
                a = a.concat(array(arguments)); //然后增加所有的内部实参
                return f.apply(this, a); //然后基于这个实参列表调用f()
            };
        }
        //这个函数的实参传递至右侧
        function partialRight(f/*,...*/) {
            var args = arguments; //保存外部实参数组
            return function () {
                var a = array(arguments); //从内部参数开始
                a = a.concat(array(args, 1)); //然后从外部第1个args开始添加
                return f.apply(this, a); //最后基于这个实参列表调用f()
            };
        }
        //这个函数的实参被用做模板
        //实参列表中的undefined值都被填充
        function partial(f/*,...*/) {
            var args = arguments; //保存外部实参数组
            return function () {
                var a = array(args, 1); //从外部args开始
                var i = 0, j = 0;
                //遍历args,从内部实参填充undefined值
                for (; i < a.length; i++)
                    if (a[i] === undefined) a[i] = arguments[j++];
                //现在将剩下的内部实参都追加进去
                a = a.concat(array(arguments, j))
                return f.apply(this, a);
            };
        }
        //这个函数带有三个实参
        var f = function (x, y, z) { return x * (y - z); };
        //注意这三个不完全调用之间的区别
        partialLeft(f, 2)(3, 4)  //=>-2:绑定第一个实参:2*(3 - 4
        partialRight(f, 2)(3, 4) //=>6:绑定最后一个实参:3*(4 -2)
        partial(f, undefined, 2)(3, 4) //=>-6:绑定中间的实参:3*(2-4)

记忆函数

内容要点:   可以将上次的计算结果缓存起来。在函数式编程当中,这中缓存技巧叫做 "记忆"。

//返回f()的带有记忆功能的版本
//只有当f()的实参的字符串表示都不相同它才会工作
function memorize(f) {
        var cache = {}; //将值保存在闭包内
        return function () {
        //将实参转换为字符串形式,并将其用做缓存的键
         var key = arguments.length + Array.join.call(arguments, ",");
         if (key in cache) return cache[key];
         else return cache[key] = f.apply(this, arguments);
  };
}

七、类和模块

这两位作者将的很详细

类:犀牛书学习笔记(9):继承 - JavaShuo

模块:犀牛书学习笔记(10):模块和命名空间-CSDN博客

八、正则表达式

js中正则的使用方法

一个是字符串的search方法

比如"javascript".search(/script/i);

第二个是replace(),用于执行检索和替换,有俩个参数,分别是正则和需要替换的字符串。如果第一个是字符串不是正则式,那么它会直接搜索这个字符串而不会先处理成正则再操作。

关于replace,还可以对子表达式进行操作,而不影响字符串其他内容,比如书中的这个例子,用中文半角引号替换英文引号。

<script type="text/javascript">


var str="Visit Microsoft!"
str.replace(/Microsoft/, "W3School")


</script>

输出:

Visit W3School!

第三个是match()方法,用于返回有匹配结果组成的数组。

第四个是split()方法,这个经常使用,用 来分割字符串,但是这个里面也能写正则式。

regexp对象

正则在js中是通过regexp对象来表示的,第十章的后面基本就是在讲这个对象了。

关于regexp对象,它有俩个参数,一个是正则//中间的文本信息,第二个是img三个字符的组合,同时要注意,这里的\也要用转义来转一下,就是说,要把\变成\才能正常使用。

regexp有5个属性,一个是字符串,正则本身的信息,3个判断是否为img的属性,还有一个记录位置的lastindex属性,这个属性用于记录匹配到的位置。

regexp有俩个方法,一个是exec(),这个主要用来匹配正则,以及输出匹配结果,另一个是test(),test只用来判断字符串中有没有能匹配成功的,有匹配的就返回true。

九、子级和扩展

1.解构赋值

Spidermonkey系javascript1.7实现了一种混合式赋值,我们称之为“解构赋值”。你可能在python或者ruby中接触过此概念,等候右侧是一个数组或对象(解构话的值),指定左侧一个或多个变量的语法和右侧数组对象直接量和语法格式一致。

发生解构赋值时,右侧的数组或对象中的一个/多个值就会被提取出来(解构),并赋值给左侧相应的变量名。除了常规赋值运算外,解构赋值还用以初始化用var和let新生命的变量。

和数组配合使用时,解构赋值是一种写法简单且有极其强大的功能。特别是在函数返回一组结果的时候解构赋值就显得非常有用。然而当配合对象或者嵌套对象一起使用时,解构赋值变得更加复杂且容易搞混。下面的例子展示了简单的和复杂的解构赋值:

let [x, y] = [1, 2]; // 等价于 let x=1,y=2
[x, y] = [x + 1, y + 1]; //等价于 x = x+1,y=y+1
[x, y] = [y, x]; // 交换两个变量的值
console.log([x, y]); //输出 [3,2]

当函数返回一组结果时,解构赋值方式将大大简化程序代码:

 // 将 [x,y] 从笛卡尔(直角)坐标转换为 [r,theta] 极坐标
            function polar(x, y) {
                    return [Math.sqrt(x * x + y * y), Math.atan2(y, x)];
                }
                // 将极坐标转换为笛卡尔坐标


            function cartesian(r, theta) {
                return [r * Math.cos(theta), r * Math.sin(theta)];
            }
            let [r, theta] = polar(1.0, 1.0); // r=Math.sqrt(2), theta=Math.PI/4
            let [x, y] = cartesian(r, theta); // x=1.0, y=1.0

2.迭代

mozilla的javascript扩展引入了一些新的迭代机制,包括for each循环和python风格的迭代器(iterator)和生成器(generator)。本文小节中将逐步介绍。

i.for/each循环

for/each循环是由e4x规范(ecmascript for xml)定义的一种新的循环语句。e4x是语言的扩展,它允许javascript程序中直接出现xml标签,并定义了操作xml数据的语法和api。浏览器大都没有实现e4x,但是mozilla javascript 1.6(随着firefox 1.5发布)是支持e4x的。本节我们只对for/each作讲解,并不会涉及到xml对象。关于e4x的剩余内容请参照11.7节:

for each循环和for/in循环非常类似。但for each并不是遍历对象的属性,而是对属性的值作遍历:

   let o = {
                one: 1,
                two: 2,
                three: 3
            }
            for (let p in o) console.log(p); // for/in: 输出 'one', 'two', 'three'
            for each(let v in o) console.log(v);

当使用数组时,for/each循环遍历的元素(而不是索引),它通常按照数值顺序枚举它们,但实际上这并不是标准化或必须的:

注意,for/each循环并不仅仅针对数组本身的元素作遍历,它也会遍历数组中所有可枚举属性,包括继承来的可枚举的方法。因此,通常并不推荐for/each循环和数组一起使用。在ECMAScript5 之前的 javascript 版本中是可以这样用的,因为自定义属性和方法不可能设置为可枚举的。

 a = ['one', 'two', 'three'];
            for (let p in a)
                console.log(p); //=>0,1,2 数组索引:
            for each(let v in a)
             console.log(v); //=>'one', 'two', 'three' 数组元素

ii.迭代器

迭代器是一个对象,这个对象允许对它的值的集合作遍历,并保持任何必要的状态以便能够跟踪到当前遍历的“位置”。

迭代器必须包含next()方法,每一次对next()调用都返回集合中的下一个值。比如下面的counter()函数返回了一个迭代器,这个迭代器每次调用next()都会返回连续递增的整数。需要注意的是,这个函数利用闭包的特性实现了计数器状态的保存:

  function counter(start) {
                let nextValue = Math.round(start); // 表示迭代器状态的一个私有成员
                return {
                    next: function() {
                        return nextValue++;
                    }
                }; // 返回迭代器对象
            }
            let serialNumberGenerator = counter(1000);
            let sn1 = serialNumberGenerator.next(); // 1000
            let sn2 = serialNumberGenerator.next(); // 1001

迭代器用于有限的集合时,当所有的值都遍历完成没有多余的值可迭代时,再调用next()方法会抛出stopiteration。stopiteration是javascript 1.7中的全局对象的属性。它是一个普通的对象(它自身没有属性),只是为了终结迭代的目的而保留的一个对象。注意,实际上,stopiteration并不是像typeerror()和rangeerror()这样的构造函数。比如,这里实现了一个rangeiter()方法,这个方法返回了一个可以对某个范围的整数进行迭代的迭代器:

 // 这个函数返回了一个迭代器,它可以对某个区间内的整数作迭代
            function rangeIter(first, last) {
                    let nextValue = Math.ceil(first);
                    return {
                        next: function() {
                            if (nextValue > last) throw StopIteration;
                            return nextValue++;
                        }
                    };
                }
                // 使用这个迭代器实现了一个糟糕的迭代.
            let r = rangeIter(1, 5); // 获得迭代器对象
            while (true) { // 在循环中使用它
                try {
                    console.log(r.next()); // 调用  next() 方法
                } catch (e) {
                    if (e == StopIteration) break; // 抛出 StopIteration 时退出循环
                    else throw e;
                }
            }
"],

3.函数简写

对于简单的函数,JavaScript 1.8引入了一种简写形式:“表达式闭包”。如果函数只包含一个表达式并返回它的值,关键字return和花括号都可以省略,并将待计算的表达式放在参数列表之后,这里有一些例子:

let succ = function(x) x + 1,yes = function() true,no = function() false;

这只是一种简单的快捷写法,用这种形式定义的函数其实和带花括号和关键字return的函数完全一样,这种快捷写法更适用于当给函数传入另一个函数的场景,比如:

    data.sort(function(a, b) b - a);
             // 定义一个函数,用以返回数组元素的平方和
            let sumOfSquares = function(data)
             Array.reduce(Array.map(data, function(x) x * x), function(x, y) x + y);

4.多catch从句

在javascript1.5中,try/catch语句已经可以使用多catch从句了,在catch从句的参数中加入关键字if以及一个条件判断表达式:

            try {
                // 这里可能会抛出多种类型的异常
                throw 1;
            } catch (e
                if e instanceof ReferenceError) {
                // 这里处理引用错误
            } catch (e
                if e === "quit") {
                // 这里处理字符串是“quit”的情况
            } catch (e
                if typeof e === "string") {
                // 处理其他字符串的情况
            } catch (e) {
                // 处理余下的异常情况
            } finally {
                // finally从句正常执行
            }

当产生了一个异常时,程序将会尝试执行每一个catch从句。catch从句中的参数即是这个异常,执行到catch的时候会它的计算条件表达式。如果条件表达式计算结果为true,则执行当前catch从句中的逻辑,同时跳过其他的catch从句。如果catch从句中没有条件表达式,程序会假设它包含一个if true的条件,如果它之前的catch从句都没有被激活执行,那么这个catch中的逻辑一定会执行。如果所有的catch从句都包含条件,但没有一个条件是true,那么程序会向上抛出这个未捕获的异常。注意,因为catch从句中的条件表达式已经在括号内了,因此也就不必像普通的条件句一样再给他包裹一个括号了。

注意:

<script type="text/javascript; version=1.5" >

5.e4x:ECMAScript for XML

“ECMAScript for XML”简称E4X,是JavaScript的一个标准扩展(注:E4X是由ECMA-357规范定义的。可以从这里查看官方文档:http://www.ecma-international.org/publications/standards/Ecma-357.htm),它为处理XML文档定义了一系列强大的特性。Spidermonkey 1.5和Rhino 1.6已经支持E4X。由于多数浏览器厂商还未支持E4X,因此E4X被认为是一种基于Spidermonkey或Rhino引擎的服务器端技术。

E4X将XML文档(元素节点或属性)视为一个XML对象,将XML片段视为一个紧密相关的XML列表对象。本节会介绍创建和使用XML对象的一些方法。XML对象是一类全新的对象,E4X中定义了专门的语法来描述它(接下来会看到)。我们知道,除了函数之外所有标准的JavaScript对象的typeof运算结果都是“object”。正如函数和原始的JavaScript对象有所区别一样,XML对象也和原始JavaScript对象不同,对它们进行typeof运算的结果是“xml”。在客户端JavaScript中(参照第15章),XML对象和DOM(文档对象模型)对象没有任何关系,理解这一点非常重要。E4X标准也针对XML文档元素和DOM元素之间的转换作了规定,这个规定是可选的,Firefox并没有实现它们之间的转换。这也是E4X更适用于服务器端编程的原因。

本小节中我们会给出一个E4X的快速入门教程,而不会作更深入的讲解。XML对象和XML列表对象的很多方法本书中并未介绍。在参考手册部分也不会对其作讲解,如果读者希望进一步了解E4X,可以参照官方文档。

E4X只定义了很少的语言语法。最显著的当属将XML标签引入到JavaScript语言中。可以在JavaScript代码中直接书写XML标签直接量,比如:

   // 创建一个XML对象
            var pt = <periodictable>
                <element id = "1"> <name> Hydrogen </name></element>
                <element id = "2"> <name> Helium </name></element>
                <element id = "3"> <name> Lithium </name></element>
                </periodictable>;


                // 给这个表格添加一个新元素
             pt.element += <element id = "4"> <name> Beryllium </name></element> ;

XML直接量语法中使用花括号作为变量输出,我们可以在XML中嵌入JavaScript表达式。例如,这里是另外一种创建XML元素的方法:

pt = <periodictable></periodictable>; // 创建一个新表格
var elements = ["Hydrogen", "Helium", "Lithium"]; // 待添加的元素
// 使用数组元素创建XML元素
for(var n = 0; n < elements.length; n++) {
    pt.element += <element id={n+1}><name>{elements[n]}</name></element>;

除了使用直接量语法,我们也可以将字符串解析成XML,下面的代码为上段代码创建的节点增加了一个新元素:

pt.element += new XML('<element id="5"><name>Boron</name></element>');

当涉及到XML片段的时候,使用XMLList()替换XML():

pt.element += new XMLList('<element id="6"><name>Carbon</name></element>' +
    '<element id="7"><name>Nitrogen</name></element>');

E4X提供了一些显而易见的语法用以访问所创建的XML文档的内容:

var elements = pt.element;      // 得到element列表
var names = pt.element.name;    //得到所有的name标签
var n = names[0];           //"Hydrogen"(氢),name的第零个标签的内容

E4X同样为操作XML对象提供了语法支持,点点(..)运算符是“后代运算符”(descendant operator),可以用它替换普通的点(.)成员访问运算符:

 

E4X甚至定义了通配符运算:

// 得到所有<element>标签的所有子节点
// 这也是得到所有<name>标签的另外一种方法
names3 = pt.element.*;

E4X中使用字符@来区分属性名和标签名(从XPath中借用过来的语法)。比如,你可以这样来获得一个属性:

// “氮”的原子序数是多少
var atomicNumber = pt.element[1].@id;

可以使用通配符来获得属性名@*,

// 获得所有的<element>标签的所有属性
var atomicNums = pt.element.@*;

E4X甚至包含了一种强大且及其简洁的语法用来对列表进行过滤,过滤条件可以是任意谓词表达式:

// 对所有的element元素组成的列表进行过滤
// 过滤出那些id属性小于3的元素
var lightElements = pt.element.(@id < 3);


// 对所有的element元素组成的列表进行过滤
// 过滤出那些name以B开始的元素。
// 然后得到过滤后元素的<name>标签列表
var bElementNames = pt.element
  .filter(function(element) {
    return element.name.startsWith('B');
  })
  .map(function(element) {
    return element.name;
  });

本章的11.4.i中讲到for/each循环是非常有用的,但在E4X标准中对for/each循环有了新的定义,可以用for/each来遍历XML标签和属性列表。for/each和for/in循环非常类似,for/in循环用以遍历对象的属性名,for/each循环用以遍历对象的属性值:

// 输出元素周期表中的每个元素名
for each (var e in pt.element) {
    console.log(e.name);
}

// 输出每个元素的原子序数
for each (var n in pt.element.@*) console.log(n);

E4X表达式可以出现在赋值语句的左侧,可以用它来对已存在的标签和属性进行修改或添加新标签或属性:

// 修改氢元素的<element>标签,给它添加一个新属性
// 像下面这样添加一个子元素:
//
// <element id="1" symbol="H">
//   <name>Hydrogen</name>
//   <weight>1.00794</weight>
// </element>
//
pt.element[0].@symbol = "H";
pt.element[0].weight = 1.00794;

通过标准的delete运算符也可以方便地删除属性和标签:

delete pt.element[0].@symbol; // 删除一个属性
delete pt..weight;      //删除所有的<widget>标签

我们可以通过E4X所提供的语法来进行大部分的XML操作。E4X同样定义了用于调用能够XML对象的方法,例如,这里用到了insertChildBefore()方法:

pt.insertChildBefore(pt.element[1],
    <element id="1"><name>Deuterium</name></element>);

E4X中是完全支持命名空间的,它为使用XML命名空间提供了语法支持和API支持:

// 声明默认的命名空间:default xml namespace = "http://www.w3.org/1999/xhtml";
// 这里是一个包含了一些svg标签的xhtml文档
d = <html>
    <body>
     This is a small red square:
     <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
         <rect x="0" y="0" width="10" height="10" fill="red"/>
     </svg>
    </body>
    </html>

// body元素和它的命名空间里的uri以及他的localName
var tagname = d.body.name();
var bodyns = tagname.uri;
var localname = tagname.localName;

// 选择<svg>元素需要多做一些工作,因为<svg>不在默认的命名空间中,
// 因此需要为svg创建一个命名空间,并使用::运算符将命名空间添加至标签名中
var svg = new Namespace('http://www.w3.org/2000/svg');
var color = d..svg::rect.@fill // "red"

声明: 因为写的时间比较久了借鉴的各个博主的文章较多,就不一一列举了,感谢

  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值