JavaScript中的引用类型Function类型学习心得

1没有重载

将函数名想象为指针,有助于理解为什么ECMAScript中没有函数重载的概念

function A(num){
    return num + 100;
}
function A(num){
    returnnum + 200;
}
console.log(A(100))//300

显然后面的函数覆盖了前面的函数

以上代码和下面的代码没什么区别

var A=function(num){
    return num + 100;
}
 A=function(num){
    return num + 200;
}
console.log(A(100))

 

第二个函数覆盖了第一个函数的变量A

2函数声明和函数表达式,Function构造函数

解析器在想执行环境家在数据时,对函数声明和函数表达式并非一视同仁,他会率先读取函数声明,并使其在执行任何代码之前可以访问(变量提升),至于函数表达式,则必须等到解析器执行到它所在的代码行才会真正被执行。

console.log(A(10,10));
function A(a,b){
    return a+b;
}

不会报错,解析器已经通过一个名为函数声明提升的过程,读取并将函数声明添加到执行环境,并放到代码树的顶部,即使在最后面也会提升到顶部。然而函数表达式却不同,会报错。

console.log(A(10,10));
var A=function(a,b){
    return a+b;
}

错误信息:

TypeError: A is not a function

var print= function x(){
    console.log(typeof x);
};
x
// ReferenceError: x is not defined
print
()
// function

上面代码在函数表达式中,加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。

Function构造函数方式创造

var add =new Function(
    'x',
    'y',
    'return (x+y)'
);
等同于
function add(x,y){
    return (x+y);
}

在上面代码中,Function构造函数接受三个参数,除了最后一个参数是add函数的“函数体”,其他参数都是add函数的参数。

你可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。

var foo =new Function(
    'return "hello world"'
);
// 等同于
function foo() {
    return'hello world';
}

函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。
function fib(num) {
    if (num > 2) {
        return fib(num - 2)+ fib(num - 1);
    } else {
        return 1;
    }
}
fib(6) // 8
上面代码中,fib函数内部又调用了fib,计算得到斐波那契数列的第6个元素是8。

 

 

3作为值的函数

函数本身就是变量,所以函数也可以作为值来使用,也就是说,不仅可以想传参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。

function A(testfunction,C){
    return testfunction(C);
}
function add(num){
    return num+10;
}
console.log(A(add,10));

也可以从一个函数返回另一个函数,而且这也是极为有用的一种技术,

function creat(pro){
    return function (obj1,onj2) {
        var v1 = obj1[pro];
        var v2 = obj2[pro];

        if(v1<v2){
            return -1;
        }else if(v1>v2){
            return 1;
        }else{
            return 0;
        }
    }
}

在内部函数接收到pro参数后,他会使用方括号标示法来取得给定属性的值。取得了想要的属性值后,定义比较函数就简单了。

var data=[{"name":"zds",age:30},{"name":"ads",age:40}];
data.sort(creat("name"));
console.log(data[0].name);//ads

data.sort(creat("age"));
console.log(data[0].name);//zds

4函数内部属性

在函数内部,有两个特殊的对象,arguments和this。arguments是一个类数组对象,包含着传入函数中的所有参数。这个arguments 还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数

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

这个求阶乘的函数这样定义是有问题的,函数的执行和函数名紧紧的耦合在一起了,如下改动

function factorial(num){
    if(num<=1){
        return 1;
    }else{
        return num*factorial(num-1);
    }
}
var s = factorial;
factorial= function () {
    return 0;
};
console.log(s(5));    //0
如果改为arguments.callee
function factorial(num){
    if(num<=1){
        return 1;
    }else{
        return num*arguments.callee(num-1);
    }
}
var s = factorial;
factorial= function () {
    return 0;
};
console.log(s(5));   //120
在此,s获得了factorial的值实际上是另一个位置尚保存了一个函数的指针。然后,我们又将一个简单的返回0的函数复制给factorial变量,解除耦合之后,仍能正常的计算阶乘。
函数内部另一个特殊的对象是this,其行为于java的this相似,this
引用的是函数数据以执行的环境对象(在网页this就是window)
window.color='red';
var o = {color:'blue'};

function say(){
    console.log(this.color);
}
say();//red
o.say = say;
console.log(o.say());//blue
 
es5引如另一个函数对象属性:caller。如果是全局作用于调用了当前函数,他的值为null,如下:
function outer(){
    inner();
}
function inner(){
    console.log(inner.caller);
}
console.log(outer());//[Function: outer]
                     //undefined
也可以实现更松散的耦合
function outer(){
    inner();
}
function inner(){
    console.log(arguments.callee.caller);
}
console.log(outer());//[Function: outer]
                     //undefined
 

5函数属性和方法

函数是对象,函数也有属性和方法。每个函数都包含两个属性:length和prototype,length表示当前函数接受参数的个数,prototype属性是保存所有实例方法的真正所在。换句话说,诸如tostring(),valueof()等方法实际上都保存在prototype下,只不过是通过各自独享的实例访问,在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的,prototype属性是不可枚举的,因此使用for-in无法发现

每个函数都包含两个非继承的方法apply和call。改变函数内this的指向。Apply()方法接受两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中第二个参数可以是array的实例,也可以是arguments对象。

function sum(n1,n2){
    return n1+n2;
}
function callsum(n1,n2){
    return sum.apply(this,arguments);
}
function callsum2(n1,n2){
    return sum.apply(this,[n1,n2]);
}
console.log(callsum(10,10));
console.log(callsum2(10,10));

call()方法和apply()方法相同,他们的区别是接受参数的方式不同,对于call()方法。第一个参数是this不变,变化的是其余参数直接传递给函数。换句话说,在使用call时,传递给函数的参数必须做个列举出来,如下

function callsum(n1,n2){
    return sum.call(this,n1,n2);
}

至于是使用apply还是call,完全决定你采用哪种方式穿参数,如果你打算直接传入arguments对象,那么使用apply,否则使用call

事实上,apply真正的用武质地是能够扩充函数赖以运行的作用域.如下

window.color = 'red';
var o = {color:'blue'};
function say(){
    console.log(this.color);
}
say();
say().call('1===',this);//red
say().call('2===',window);//red
say().call('3===',o);//blue

使用call和apply来扩充作用于的最大好处就是对象不需要与方法有任何耦合关系。

Array.prototype.slice.call()//可以将arguments这种伪数组转化为真正的数组

 

Es5还有一个bind()方法,这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值,如下:

window.color = 'red';
var o = {color:'blue'};
function say(){
    console.log(this.color);
}
var onj1 = say.bind(o);
onj1();//blue

bind传入o参数,返回一个函数,这个返回的函数的this值等于o

varobj = {

    x: 81,

};  

varfoo = {

    getX: function() {

        returnthis.x;

    }

}  

console.log(foo.getX.bind(obj)());  //81

console.log(foo.getX.call(obj));    //81

console.log(foo.getX.apply(obj));  //81

三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。

  也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。

再总结一下:

       apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;

       apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;

       apply 、 call 、bind 三者都可以利用后续参数传参;

              bind 是返回对应函数,便于稍后调用;apply、call 则是立即调用 。

 

6不能在条件语句中声明函数

根据ECMAScript的规范,不得在非函数的代码块中声明函数,最常见的情况就是if和try语句。

if (foo) {
    function x() {}
}

try {
    function x() {}
} catch(e) {
    console.log(e);
}
上面代码分别在if代码块和try代码块中声明了两个函数,按照语言规范,这是不合法的。但是,实际情况是各家浏览器往往并不报错,能够运行。

但是由于存在函数名的提升,所以在条件语句中声明函数,可能是无效的,这是非常容易出错的地方。

if (false) {
    function f() {}
}

f() // 不报错
上面代码的原始意图是不声明函数f,但是由于f的提升,导致if语句无效,所以上面的代码不会报错。要达到在条件语句中定义函数的目的,只有使用函数表达式。

if (false) {
    var f = function (){};
}

f() // undefined

6-1函数的属性和方法

name属性
name属性返回紧跟在function关键字之后的那个函数名。

function f1() {}
f1.name // 'f1'

var f2 = function () {};
f2.name // ''

var f3 = function myName() {};
f3.name // 'myName'
上面代码中,函数的name属性总是返回紧跟在function关键字之后的那个函数名。对于f2来说,返回空字符串,匿名函数的name属性总是为空字符串;对于f3来说,返回函数表达式的名字(真正的函数名还是f3,myName这个名字只在函数体内部可用)。

length属性
length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。

function f(a, b) {}
f.length // 2
上面代码定义了空函数f,它的length属性就是定义时的参数个数。不管调用时输入了多少个参数,length属性始终等于2。

length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的”方法重载“(overload)。

toString()
函数的toString方法返回函数的源码。

function f() {
    a();
    b();
    c();
}

f.toString()
// function f() {
//  a();
//  b();
//  c();
// }
函数内部的注释也可以返回。

function f() {/*
 这是一个
 多行注释
 */
}

f.toString()
// "function f(){/*
//   这是一个
//   多行注释
// */}"
利用这一点,可以变相实现多行字符串。

var multiline = function (fn) {
    var arr = fn.toString().split('\n');
    return arr.slice(1, arr.length- 1).join('\n');
};

function f() {/*
 这是一个
 多行注释
 */
}

multiline(f);
// " 这是一个
//   多行注释"

 

 

7函数作用域

作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。

与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

var a =1;
var x = function () {
    console.log(a);
};
function f() {
    var a = 2;
    x();
}
f() // 1

默认值

通过下面的方法,可以为函数的参数设置默认值。

function f(a){

  a = a || 1;

  return a;

}

 

f('') // 1

f(0) // 1

上面代码的||表示“或运算”,即如果a有值,则返回a,否则返回事先设定的默认值(上例为1)。

这种写法会对a进行一次布尔运算,只有为true时,才会返回a。可是,除了undefined以外,0、空字符、null等的布尔值也是false。也就是说,在上面的函数中,不能让a等于0或空字符串,否则在明明有参数的情况下,也会返回默认值。

为了避免这个问题,可以采用下面更精确的写法。

function f(a) {

  (a !== undefined&& a !== null) ? a = a : a = 1;

  return a;

}

 

f() // 1

f('') // ""

f(0) // 0

上面代码中,函数f的参数是空字符或0,都不会触发参数的默认值。

传递方式

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。

var p = 2;

 

function f(p) {

  p = 3;

}

f(p);

 

p // 2

上面代码中,变量p是一个原始类型的值,传入函数f的方式是传值传递。因此,在函数内部,p的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。

但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

var obj = {p: 1};

 

function f(o) {

  o.p = 2;

}

f(obj);

 

obj.p // 2

上面代码中,传入函数f的是参数对象obj的地址。因此,在函数内部修改obj的属性p,会影响到原始值。

注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

var obj = [1, 2, 3];

 

function f(o){

  o = [2, 3, 4];

}

f(obj);

 

obj // [1, 2, 3]

上面代码中,在函数f内部,参数对象obj被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o)与实际参数obj存在一个赋值关系。

// 函数f内部

o = obj;

上面代码中,对o的修改都会反映在obj身上。但是,如果对o赋予一个新的值,就等于切断了o与obj的联系,导致此后的修改都不会影响到obj了。

某些情况下,如果需要对某个原始类型的变量,获取传址传递的效果,可以将它写成全局对象的属性。

var a = 1;

 

function f(p) {

  window[p] = 2;

}

f('a');

 

a // 2

上面代码中,变量a本来是传值传递,但是写成window对象的属性,就达到了传址传递的效果。

 

8参数

默认值

通过下面的方法,可以为函数的参数设置默认值。

function f(a){

  a = a || 1;

  return a;

}

 

f('') // 1

f(0) // 1

上面代码的||表示“或运算”,即如果a有值,则返回a,否则返回事先设定的默认值(上例为1)。

这种写法会对a进行一次布尔运算,只有为true时,才会返回a。可是,除了undefined以外,0、空字符、null等的布尔值也是false。也就是说,在上面的函数中,不能让a等于0或空字符串,否则在明明有参数的情况下,也会返回默认值。

为了避免这个问题,可以采用下面更精确的写法。

function f(a) {

  (a !== undefined&& a !== null) ? a = a : a = 1;

  return a;

}

 

f() // 1

f('') // ""

f(0) // 0

上面代码中,函数f的参数是空字符或0,都不会触发参数的默认值。

同名参数

如果有同名的参数,则取最后出现的那个值。

function f(a, a) {

  console.log(a);

}

 

f(1, 2) // 2

上面的函数f有两个参数,且参数名都是a。取值的时候,以后面的a为准。即使后面的a没有值或被省略,也是以其为准。

function f(a, a){

  console.log(a);

}

 

f(1) // undefined

调用函数f的时候,没有提供第二个参数,a的取值就变成了undefined。这时,如果要获得第一个a的值,可以使用arguments对象。

function f(a, a){

  console.log(arguments[0]);

}

 

f(1) // 1

 

9函数其他知识点

闭包

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

要理解闭包,首先必须理解变量作用域。前面提到,JavaScript有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。

var n = 999;

 

function f1() {

  console.log(n);

}

f1() // 999

上面代码中,函数f1可以读取全局变量n。

但是,在函数外部无法读取函数内部声明的变量。

function f1() {

  var n = 999;

}

 

console.log(n)

// Uncaught ReferenceError: n is not defined(

上面代码中,函数f1内部声明的变量n,函数外是无法读取的。

如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

function f1() {

  var n = 999;

  function f2(){

  console.log(n); // 999

  }

}

上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是JavaScript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

function f1() {

  var n = 999;

  function f2(){

    console.log(n);

  }

  return f2;

}

 

var result = f1();

result(); // 999

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

function createIncrementor(start) {

  return function() {

    return start++;

  };

}

 

var inc = createIncrementor(5);

 

inc() // 5

inc() // 6

inc() // 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

为什么会这样呢?原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

闭包的另一个用处,是封装对象的私有属性和私有方法。

function Person(name) {

  var _age;

  function setAge(n){

    _age = n;

  }

  function getAge(){

    return _age;

  }

 

  return {

    name: name,

    getAge: getAge,

    setAge: setAge

  };

}

 

var p1 = Person('张三');

p1.setAge(25);

p1.getAge() // 25

上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值