《JS高级程序设计-第3版》的读书笔记关于函数

1、js的数据类型(这里用js代指ECMAScript)

js共有6种数据类型。其中基本数据类型5个为Undefined、Null、Boolean、Number、String。引用类型1个Object,Object本质是由一组无序的键值对构成。其中所有类型的构建都可以用var(let?)关键字来声明。这样统一的声明方式来源于js的变量是松散类型,及每个变量只是一个用于保存值的占位符。当不用var时即为全局变量,归Global管,浏览器都是将Global对象作为window的一部分实现,即window可认为是Global的实现与扩展。

2、类型的区分与简介

js的六种基本类型都可以用typeof区分。typeof的运算结果可以为

"undefined"——值未定义;在变量声明后默认为undefined。与未定义不同。派生自null

"boolean"——值为布尔值;js中所有类型的值都有与true和false等价的值。例如非0,0,"","1".

"string"——值为字符串;16位Unicode组成。其中length表明的是有几个16位。所以可能与实际有偏差。

"number"——值为数值;js只有整形和浮点型。其中-0和+0相等。还有NaN,(-)Infinity 无穷。

"object"——值为对象或null;测试null时返回object,因为null值表示的是一个空对象的指针。

"function"——值为函数;多种声明方式,按值传参。函数名也只是个指针,可以复制别的变量。

其中函数在js中是Function类的对象,但是返回function。typeof测试null返回object,是空对象的指针。

3、js中的函数

 js的函数用function关键字来声明,格式如下。js中使用var时没有块级作用域(if,for这样的花括号括起来的部分中声明的变量和在外面声明是一样的),但函数有函数作用域(即局部作用域)。当使用let声明变量后会存在块级作用域,也附带了许多限制。

function functionName(arg0, arg1, ... ,argN){  //这种是函数声明
    statements
}

var sum = function(num1, num2){    //这种是函数表达式,等号右边为匿名函数
    return num1+num2;
};   //这有个分号。

函数声明的方式,在代码执行前,解析器会有函数声明提升的过程,即把函数提升到代码书的顶部(这样可以先使用后定义,对于变量也同样适用,变量的声明可以提升,先使用后定义,但是对于var x=1;这样的表达式不能提升。变量会被提升的更高。函数形参-->函数声明-->变量声明。只有声明会提升,var x;会提升,其余都不变。),函数表达式的方式没有(可以放在判断语句中,条件性的定义函数,因为不会提升),因为它只是一条初始化语句。还有一种函数的定义方式,是使用Function类的构造函数,该构造函数可以接收任意数量的参数,(原因在下一个代码段下面)但是最后一个参数被看作函数体,前面的参数则代表了新创建函数实例的形参。所以其声明创建的格式如下。

var sum = new Function("num1", 'num2', "return num1+num2");  //不推荐

这种方式的结果与上面两个相同,但是会解析两次代码,效率降低。第一次解析js的代码,第二次解析传入构造函数的字符串。js中变量的定义,都是使用var(let?),所以在形参部分直接命名就行,这些形参就是函数的命名参数。js中函数是对象,从最后一个函数构造的例子中可以很明显的看出。其中函数名即为该函数的指针。可以直接赋值给别的变量,例如:

function sayHello(name){
    alert("Hello "+name);
}

var x = sayHello;

x("123");

 函数中的特殊对象(1)arguments:在js中的函数的参数有一个特点。即js的函数可以接受任意多个实参,不论是什么类型。即便在定义的时候设定了几个参数,在使用的时候都可以传递多于设定的参数或少于设定的参数,甚至不传参数。原因是在js中,参数在内部的是用一个数组表示,而函数接收到的是这个数组,实参到形参的传递只是给数组赋值的过程。但命名变量任然存在,与该数组拥有各自的内存,但命名参数于该数组中的值是同步的,修改任一个,另一个都会改变。在函数体内可以通过arguments对象来访问这个数组。arguments对象只是与数组类似(它并不是Array的实实例),但可以使用"[]"语法来访问,它的length属性可以确定传进来参数的数量。arguments.length即可。

function sayHello(){
    alert("Hello "+arguments[0]+arguments[1]);
}

在定义时不定义任何的形参,但是在使用的时候可以这样:sayHello("123","456");这次的函数调用说明了,函数的命名参数(形参)只是为了在函数内能够方便访问到参数,但是不是必须的。在函数的调用时也不会进行函数签名的验证,所以可以传入数量变化的参数。这使得js以另一种方式实现了重载的功能。但js中的函数没有重载的功能。说明如下

var sum = new Function("num", 'return num+10');

sum = new Function('num', "return num+20");

函数的重载其实只是改变了sum中保存的指针的值。

函数的这个arguments对象的另外一个值得注意的地方是一个属性callee(不是函数的属性),该属性是一个指针,指向拥有这个arguments对象的函数。或者说指向正在执行的函数。请看下面这个非常经典的阶乘函数。

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

定义结成函数一般要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后也不会变的情况下,这样的定义没有问题。但问题是这个函数的执行与函数名factorial紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用arguments.callee。

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

摘抄结束。这样,就可以将保存函数指针的变量由factorial替换为别的变量,而不发生错误。

函数中的特殊对象(2)this:this引用的是函数据以执行的环境对象。全局作用域中就是window。或者说,哪个对象调用的这个函数,那么这个函数中的this就代表了这个对象,简单的判断就是看函数前的对象,如果没有就是window,也就是说在函数1中调函数2,如果函数2是直接调用的,那么函数2中的this指向window。但是无论那个对象调用这个函数,其实函数名所指的函数是同一个。this的例子如下。

window.color = "red";
var o = {color: "blue"};    //用对象字面量定义一个对象
function sayColor(){
    alert(this.color);
}

sayColor(); //red
o.sayColor = sayColor;
o.sayColor(); //blue

函数作为值(回调函数):因为函数名是变量,所以函数可以作为值使用,即可作为参数,可作为返回值。例子如下。

function callSomeFunction(someFunction, someArgument){
    return someArgument(someArgument);
}

这样定义了一个把函数名作为参数的的函数,把第一个参数作为函数名。因为函数名在没有加括号时是变量,可以直接访问。在加括号后为函数调用,所以使用的例子如下。

function add(num){
    return num+10;
}

var result = callSomeFunction(add, 10);
alert(result);    //20

 以上是将函数作为参数传递,return中也只是函数的调用,下面是将函数作为返回值,类似于java中的匿名函数。例子如下。

function createComparisonFunction(propertyName) { 

    return function(object1, object2){         
        var value1 = object1[propertyName];         
        var value2 = object2[propertyName]; 
        
        if (value1 < value2){             
            return -1;         
        } else if (value1 > value2){             
            return 1;         
        } else {             
            return 0;         
        }     
    }; 
}

var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}]; 

data.sort(createComparisonFunction("name")); 
alert(data[0].name);  //Nicholas 

data.sort(createComparisonFunction("age")); 
alert(data[0].name);  //Zachary 

这样,将函数返回前,可以动态的改变排序的参数。sort()默认按字符集的先后排序。即字母顺序。在因为存在函数中调用函数,js也存在着一个访问外层函数的属性caller,该属性保存着调用当前函数的函数的引用。在全局作用域中它的值为null。

function inner(){
    alert(inner.caller);
    //alert(arguments.callee.caller);    显示与上一行相同
}

function outer(){  
    inner();
}

outer();  //打印outer()函数的源码

上例不能在严格模式下使用。

函数的属性和方法(与上文的特殊对象不同)js中的函数是对象,所以函数存在属性与方法。

每个函数都有两个属性:length和prototype。length属性表示函数希望接收的命名参数的个数,也就是命名参数的个数。上面例子可以作为调用,outer.length,这里其值为0。prototype属性保存了引用类型的所有实例方法。例如toString(),valueOf()等都是在prototype名下,只不过是通过各自对象的实例访问罢了。

每个函数都有两个非继承而来的方法:apply()和call()。这两个方法都是在特定的作用域中调用函数,相当于设置函数内this对象的值。apply接收两个参数,一个是作用域,一个是参数数组。对于第二个参数,可以是Array实例或arguments对象。

function sum(num1, num2){
    return num1 + num2; } 

function callSum1(num1, num2){
    return sum.apply(this, arguments);    //传入arguments 对象
} 

function callSum2(num1, num2){
    return sum.apply(this, [num1, num2]);    //传入数组 
}

alert(callSum1(10,10));   //20 
alert(callSum2(10,10));   //20

两个函数均在全局作用域中调用,所以this为window对象。call()方法域apply()方法相同,区别在于接受参数的方式不同。对于call()第一个参数为this,其余的参数需要直接传递给call(),如下例子:

function sum(num1, num2){
    return num1 + num2;
} 
 
function callSum(num1, num2){
    return sum.call(this, num1, num2);    //只有参数传递方式的区别
} 
 
alert(callSum(10,10));    //20

这两者的使用,主要在于扩充函数的作用域。也就是修改this,如下

window.color = "red";
var o = { color: "blue" }; 
 
function sayColor(){
    alert(this.color);
} 

sayColor();        //red 

sayColor.call(this);    //red 因为在全局作用调用this指向window,看传参的位置所以是全局
sayColor.call(window);    //red
sayColor.call(o);        //blue

ECMAScript5中还有一个bind()方法,该方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的返回值,如下。

window.color = "red"; 
var o = { color: "blue" }; 
 
function sayColor(){
    alert(this.color);
}

var objectSayColor = sayColor.bind(o); 
objectSayColor();    //blue,即便在全局作用域中调用

每个函数继承的toLocaleString()和toString()方法始终返回函数的代码。格式因浏览器而异。(另外一个继承的valueOf()方法同样也只返回函数代码。括号内笔者未验证)

 补充:在js中有一个功能强大的方法eval()。它类似于一个完整的js解析器。只接受一个参数就是要执行的js的字符串。

eval("alert('hi')");
/*相当于*/
alert('hi');

当解析器发现代码中调用eval()方法时,他会将传入的参数作为js语句解析,然后把执行结果插入到原来的位置。并且同通过eval()执行的代码被认为是包含在该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用链。所以传入的参数可以引用更大范围作用域的变量。如下。

var msg = "hello world";
eval("alert(msg)");

反之,在eval()中定义的变量在外部不一定能够访问。在非严格模式下可以,在严格模式下则不行。另外,在eval()的变量或函数不会被提升。因为在执行到它之前,作为参数的js代码都只是字符串。那这个函数的用武之地在哪?个人已知的是,在早期json被使用的时候,作为了json-->js对象、变量、数组的转换工具,显然这是不安全的。

补充:基本类型保存的是变量的值,引用类型保存的是在内存中对象的引用。两者的操作会有区别如下。

var person = new Object();
person.name = "Nicholas";
alert(person.name);    //"Nicholas" 

以上代码创建了一个对象并保存在了变量person中,然后又为其添加了一个名为name的属性,然后赋值,访问。如果这个对象不被销毁或者这个属性不被删除,则该属性一直存在。但是对于基本类型则不行。

var name = "Nicholas";
name.age = 27;
alert(name.age);    //undefined

这样写虽然不会报错,但是却没有任何作用。

posted @ 2019-07-22 19:47  缓步徐行静不哗  阅读(244)  评论(0)    收藏  举报