复习笔记 JavaScript语言精粹 第4章 函数

114 阅读6分钟

函数

  • 函数包含一组语句,是JavaScript的基础模块单元,用于
    • 代码复用
    • 信息隐藏
    • 组合调用
    • 指定对象的行为
  • 所谓编程,就是将一组需求分解成一组函数与数据结构的技能

函数对象

  • 函数就是对象
  • 对象是名值对的集合并拥有一个连到原型对象的隐藏连接
    • 对象字面量产生的对象连接到Object.prototype
    • 函数对象连接到Function.prototype(其本身也是连接到Object.prototype)
  • 每个函数创建时会附加两个隐藏属性
    • 函数上下文
    • 实现函数行为的代码
  • 注意和隐藏连接到Function.prototype的区别.什么区别呢我也不知道
  var test = function test (){};
  console.log(test.prototype)
  • 函数是对象,所以它能像对象一样使用可以保存在

    • 变量
    • 对象
    • 数组中.
  • 可以当做参数传递给其他函数,也可以再返回函数.

  • 因为函数是对象,所以函数也可以拥有方法

  • 函数的与众不同,在于他们可以被调用

函数字面量

  • 4个部分
    • 保留字function+函数名(可省略)
    • 包括在圆括号中的一级参数(逗号隔开,调用时才被初始化为实际提供的参数,实参)
    • 包括在花括号中的一经且语句
var add = function (a,b) {
  return a+b;
};
  • 函数字面量可以出现在任何允许表达式出现的地方.
  • 可以被定义在其他函数中
  • 一个内部函数除了可以访问自己的参数和变量,也能自由访问其父函数中的参数与变量
  • 通过函数字面量创建的函数对象包含一个连到外部上下文的连接,被称为闭包....就是指上面这一行话.内部可以访问外部(父级)

调用

  • 调用函数会暂停当前函数执行,传递控制权和参数给新函数.

  • 除了声明时定义的形参,每个函数还接收两个附加的参数

    • this
    • arguments.
  • this取决于调用模式的不同而值不同

    • 方法调用模式
    • 函数调用模式
    • 构造器调用模式
    • apply调用模式
  • 调用运算符跟在任何产生一个函数值的表达式之后的一对圆括号.

  • 圆括号内可以有0或多个用逗号分开的表达式,每个表达式产生一个参数值,对应到形参上

  • 实参的个数比形参多,则忽略多的,如果少,则以undefined填充

  • 方法调用模式

    • 当函数被保存为对象的一个属性时,称之为一个方法
    • 当方法被调用时,this被绑定到该对象上
    • 如果有提取动作.,那么就是方法调用
    • 方法可以使用this访问自己所属的对象,所以能从对象中取值或进行修改
    • this的绑定发生在调用的时候,这种延迟绑定可以对this实现高度复用
    • 通过this属性取得它们对象所属的上下文的方法叫公共方法
var myObject = {
    value:0,
    increment:function(inc){
        this.value += typeof(inc) === 'number' ? inc : 1;
    }
};
myObject.increment();//方法调用模式
document.writeln(myObject.value);//=>1
myObject.increment(2);
document.writeln(myObject.value);//=>3
  • 函数调用模式
    • 当一个函数并非一个对象的属性时,那么它就是被当作一个函数来调用的
    • 函数模式调用时,this被绑定到全局对象即window
var sum = add(3,4); //  函数调用模式      
// 通过that来避免被绑定到全局变量上
myObject.double = function(){
  var helper = function(){
    this.value = add(this.value,this.value);
  };
  //因为helper是函数调用模式,所以这里的this,并不是访问的myObject里面的this.而是访问的window的this.
  helper();
};
    
myObject.double();
console.log(myObject.value); // => 3
    
myObject.double = function(){
  // 因为double是方法调用,所以这个this是myObject
  var that = this;
  helper = function(){
    that.value = add(that.value,that.value);
  };
  helper();
};
myObject.double();
console.log(myObject.value); // => 6
  • 构造器调用模式
    • javascript是基于原型继承的语言,直接从对象进行继承.所以是没有类型这一说的.
    • 主流语言都是基于类,所以javascript也引用了基于类的类似的对象构建语法
    • 在函数前面加上New来调用,则会创建一个连接到该函数的prototype成员的新对象.this就绑定到这个新对象上(就是说复制了一个函数,也不叫复制,因为新函数对象就只拥有旧函数的原型Prototype里面的东西,而旧函数里面的属性和方法都不会继承下来)
    • new 前缀也会改变return语句的行为....不懂
var Quo = function (string) {
    this.status = string;    
};
Quo.prototype.get_status = function(){
    return this.status;
};
 //因为是链接到Quo原型的,所以拥有Quo原型上属性和方法
var myQuo = new Quo("confused");
console.log(myQuo.get_status(),myQuo.status); // => confused confused  

// 因为是函数调用,所以this是指向window.status是给window添加了一个属性,这里要返回undefined....
// 因为是给prototype添加了一个get方法,所以直接调用get_status()没用.
// 因为他本身没有这个方法,即使是调用prototype.get_status()也返回undefined,因为方法里面的this还是指向了本身
Quo("xxx");
console.log(Quo.get_status(),Quo.status); // => undefined,undefined
  • 如果直接在Quo里面声明一个变量和方法,那Quo可以直接调用..这不必多说,

  • 但是new出来的myQuo就没有这个变量和方法了,因为myQuo是链接到他的prototype上的

  • 不推荐使用构造器new,,即使要用也要大写函数名

  • Apply调用模式

    • 因为javascript是函数式编程语言,所以函数可以拥有方法(c#的方法里面就不能再用方法了)
    • apply可以构建一个参数数组传递给函数,并且可以自定义this的值.
// add函数默认只接受两个参数,多的会被忽略掉
var array = [3,4,5];
//第一个参数是改变调用函数对象的this值,第二个是参数数组
var sum = add.apply(null,array);
console.log(sum); // => 7

var stutusObject = {
  status:'A-OK'
};
//相当于给stutusObject临时加上了一个get_status方法
console.log(myQuo.get_status.apply(stutusObject,[])); // => A-OK

参数

  • 函数调用时,有一个隐藏参数arguments数组.包含所有实参.
var sum = function() {
  var i, sum = 0;
  for (i = 0; i < arguments.length; i += 1) {
    sum += arguments[i];
  };
  return sum;
};
console.log(sum(1,2,3,4)); // => 10
  • 要注意的是,arguments并不是一个真正的数组
    • 只是类似数组的对象
    • 只有length属性没有数组的方法

返回

  • 当函数被调用时,它从第一个语句开始执行,并遇到关闭函数体的}结束,然后把控制权交给调用该函数的程序
  • return可提前返回函数,后面的语句不再执行
  • 函数总要返回值,如果没有指定,则返回undefined
  • 如果是构造器调用模式,返回值如果不是一个对象,那么就返回this
var te2 = new te();
console.log(te2) //=> te{}

var te = function(){
  var x = 1;
  return {};
}
var te3 = new te();
console.log(te3)// => {}

异常

  • throw中断函数执行
  • 招聘一个exception对象
  • 至少包含name和message属性
  • ....然后跳到catch里面去
var add = function(a,b){
if(typeof a !== 'number' || typeof b !== 'number'){
  throw {
    name:'TypeError',
    message:'add needs numbers'
  };
};
  return a + b;
};

try {
  add('',3);
} catch(e) {
  console.log(e);
}

扩充类型的功能

  • 给语言的基本类型扩充功能,类似于c#的扩展方法
// 给所有函数添加一个method方法
Function.prototype.method = function(name,func){
  this.prototype[name] = func;
  return this;
};
// 给Number函数添加一个integer方法
Number.method('integer',function(){
  return Math[this<0?'ceil':'floor'](this);
});
console.log((-10/3).integer(),(10/3).integer());//-3,3

String.method('trim',function(){
  return this.replace(/^\s+|\s+$/,'')
});
console.log('    neat   '.trim());
  • 因为基本类型的原型是公有结构.尽量还是不要乱加方法进去

递归

  • 这个需要专门的练习
  • 会直接或间接调用自身的一种函数
  • 汉诺塔算法,代码能够看懂,但是脑子里面逻辑走不通
var hanoi = function(disc,src,aux,dst){
  if(disc>0){
    hanoi(disc - 1,src,dst,aux);
    console.log('move disc '+disc+'from '+src +' to ' + dst);
    hanoi(disc - 1,aux,src,dst);
  }
};   
hanoi(2,'左边','中间','右边');
  • 递归可以很容易的正理树形结构 ,比如浏览器的文档对象模型DOM
  • 依次访问HTML中的每个子节点...下面是查找某个属性值为xxx的节点列表
var walk_the_DOM = function walk (node,func){
  func(node);// 在当前节点上执行方法
  node = node.firstChild;//获取第一个子节点
  while(node){ // 如果有子节点或弟弟节点
    walk(node,func);//就递归..如果子节点或弟弟节点没有子节点,就中断函数
    node = node.nextSibling; //就找弟弟节点
  }
};
var getElementsByAttribute = function(attr,value){
  var results =[];
  walk_the_DOM(document.body,function(node){
    var actual = node.nodeType ===1 && node.getAttribute(attr);//如果是元素节点就查找属性
    if(typeof actual === 'string' && (actual === value || typeof value !== 'string')){//如果有属性值且属性值和参数一样或参数值不是字符串,就保存这个节点
      results.push(node);
    }
  });
  return results;
};
console.log(getElementsByAttribute('id','a'));

尾递归

  • 就是那个什么拉..不是拉..是裴波那契数列....因为函数返回自身递归调用 的结果..就是说子函数的结果父函数要拿去用的..可以用一种循环的方式去优化...
  • javascript没有进行这样的优化
var factorial = function(i,a){
  a = a||1;
  if(i<2){
    return a;
  }
  return factorial(i-1,a*i);
}
console.log(factorial(4,'xx'));

作用域

  • 作用域控制着参数和变量的可见性及生命周期.
  • 减少了名称冲突并且提供了自动的内存管理
var foo =function(){
  var a=3,b=5;
  var bar = function(){
    var b=7,c=11;
    a +=b+c;
  };
  bar();
};
  • javascript只有函数作用域,但是没有块级作用域,即if里面声明的东西,在其外面也可以访问
  • 延迟声明变量在javascript中行不通...因为没有块级作用域,即使延迟声明和前置声明也是一样的效果.所以在函数顶部声明所有变量

闭包

  • 作用域的好处是内部函数可以访问父函数的参数和变量,除了this和auguments之外..
  • 内部函数比外部函数拥有更长的生命周期.
var myObject = (function(){ //是将return后面那个对象赋值给myObject
  var value = 0;
  return {
    increment:function(inc){
      value +=typeof(inc) === 'number'?inc:1;
    },
    getValue:function(){
      return value;
    }
  };
}()); // 这对括号是直接调用这个匿名函数
console.log(myObject.getValue());//虽然myObject里面没有value属性,但是也能够访问它,因为是子函数

和上面这个例子差不多的另外一个更明显的例子..当父函数返回后,子函数还是能够调用父函数的参数和变量,就叫闭包

var quo = function(status) {
  return {
    get_status: function() {
      return status;
    }
  };
};

var myQuo = quo('xxx');//函数执行完之后就返回了
console.log(myQuo.get_status()); //虽然quo已经返回了,但是myQuo里面的方法还是能够return status.因为作用域的关系,这就叫闭包

这是一个可见的案例

var fade = function(node){
  ar level = 1;
  var step = function(){
     var hex =level.toString(16);
     console.log(hex);
     node.style.backgroundColor ="#FFFF" + hex +hex;
     if(level<15){
       level +=1;
        setTimeout(step,300);
     }
  };
  step(step,100);
};

fade(document.getElementById("a"));

一个经常出错的用法

var add_the_handlers = function(nodes) {
  var i;
  for (i = 0; i < nodes.length; i += 1) {
    //Nodes后面的i和var i是在一个作用域里面的.所以值一直在变
        nodes[i].onclick = function(e) { 
            console.log(i);//这里只是声明了一个函数,i一直只是一个引用,值根本不会变.只有在执行的时候才会去调用i变量.那个时候i已经是Length了
        }
    };
};

var add_the_handlers = function(nodes){
    var i;
    //整个alertIndex就是一个闭包,他返回一个其名函数,接收一个e的参数..但是他的代码块,则是返回index这个参数.而这个参数,是每次循环的时候就固定赋值的    
    var alertIndex = function(index){
        return function(e){
            console.log(index);
        };
    };
    
    for(i=0;i<nodes.length;i+=1){
        nodes[i].onclick = alertIndex(i);//这里是直接调用方法,...立即就执行了这个方法,一共扫行了length次,,就不会出现上面那种情况了
    }
};

在循环内部不要创建函数.无谓的计算

回调

  • 同步执行网络请求会让浏览器限入假死状态.因为请求没回来的时候一直要卡UI线程
    • 发送请求request
    • 接收回应response......这个过程就会一直卡死浏览器
    • 渲染浏览器
  • 所以要发起异步请求,当服务器响应到达时,在异步里面立即触发回调函数,就不会阻塞客户端
    • 异步发起请求asyn request
    • 在异步方法里面接收回应 resonse...这样就不会卡死浏览器
    • 渲染浏览器
  • 怎么自定义一个回调?????..其实就是把函数做为参数传递给另一个函数就是回调..因为这样一传,函数就成了一个参数..就会延迟到他被调用的时候进行执行

模块

  • 模块是一个提供接口却隐藏状态与实现的函数或对象..
  • 可以使用函数和闭包来构造模块
  • 通过函数产生模块,可以完全摒弃全局变量的使用
  • 比如我们要把用户提交的博客内容中的html代码删除掉,则要对他的内容,按照html标签规则进行过滤..那么这个html标签规则是存在哪里呢?
  • 正常情况下是这样的
var entity ={
    quot:'"',
    lt:'<',
    gt:'>'
}
xx.onclick = function(){
  this.value.replace(/&([^&;])/g,'entity相关的规则')
};
// 这样就多出来一个entity全局变量....不推荐的说

// 但是如果写在函数里面也有问题.虽然没有了全局变量.但因为每加载一次方法,就会计算一下整个entity变量
xx.onclick = function(){
var entity ={
    quot:'"',
    lt:'<',
    gt:'>'
}
this.value.replace(/&([^&;])/g,'entity相关的规则')};

// 正确的应该是下面这种做法 String.method('deentityify',function(){ var entity ={ quot:'"', lt:'<', gt:'>' }; // 下面这个函数才是赋值给deentityify的函数,因为当前函数后面跟了一对小括号,所以是立即执行了一下.返回了下面这个函数 return function(){ return this.replace(/&([^&;]+);/g,function(a,b){ var r = entity[b]; return typeof r === 'string' ?r:a; }); }; }()); console.log("<">".deentityify());

- 定义了私有变量和函数的函数,利用闭包创建可以访问私有变量和函数的特权函数,最后返回这个特权函数......有利于减少全局变量,封闭应用程序和构造单例对象...好吧.有点小复杂....明白是明白了.运用在哪里运用呢.以后再说吧
- 也可以用来生产安全的对象,就是让外部不能访问私有变量.只能通过开放的方法来访问它..

## 级联
- 函数返回this.就是级联....就像Jquery中的修改dom属性一样.$('x').css().class().....

## 柯里化
- 就是让函数和他的参数构成一个新的函数

## 记忆
-  记住之前的操作记录,避免重复计算...这应该算是一种编程思想吧.....


所有的回调,模块,级联,柯里化和记忆都是建立在闭包基础上的.而闭包又是建立的作用域基础上的.....