林大妈的JavaScript基础知识(三):JavaScript编程(2)函数

  JavaScript是一门函数式的面向对象编程语言。了解函数将会是了解对象建立和操做、原型及原型方法、模块化编程等的重要基础。函数包含一组语句,它的主要功能是代码复用、隐藏信息和组合调用。咱们编程就是把一个需求拆分红若干函数和数据结构的组合实现,其中算法又是实现正确函数的方法论。咱们先介绍基础知识:① 在JavaScript中,函数对象背后到底有什么;② 函数调用的模式有多少种;③ 做用域与闭包。至于递归、记忆、回调、级联、模块、柯里化等,咱们放到进阶知识中再涉猎。html

1、 函数对象程序员

  前面咱们提到,在JavaScript中,函数也是对象,通常对象的原型链接到Object.prototype,函数对象则链接到Function.prototype,再链接到Object.prototype。咱们能够看看这两个对象中具备什么样的属性:web

1 var sum = function(a, b) { 2     return a + b; 3 } 4 
5 console.log(sum.prototype);

  输出发现,这个Function.prototype中有一个constructor构造器属性,值就是刚才咱们定义的这个函数的内容。而Function.prototype则链接到Object.prototype。也就是说当咱们建立一个函数对象时,Function的构造器会自动运行相似这样的一些代码: this.prototype = {constructor: this}; 。换句话说,其实constructor属性的意义不大,由于它本身自己就是这个属性。只是由于JavaScript为了模仿其余的面向对象语言,作出了这样一个“伪类”,以constructor做为中间层而已。算法

2、函数调用编程

  函数的特别之处在于:它能够被调用。调用函数时,操做系统会暂停当前函数的执行,把控制权和参数传递给调用的函数。函数除了收到给出的形式参数,还会接收两个新参数:this 和 arguments。咱们在面向对象的编程中最须要注意的就是须要用到的方法里面this的值究竟是什么。实际上,this的值取决于函数调用的模式。在JavaScript中,函数调用的模式一共有4种,分别是:方法调用模式、函数调用模式、构造器调用模式和apply调用模式:数组

  1. 方法调用模式浏览器

  对象中的函数咱们称为方法。此时,该函数中this的值为直接所属的对象。数据结构

  注:但因为设计失误,若是将对象中的方法存入一个变量中,再调用这个变量,这一个this又将指向全局变量:闭包

 1 var obj = {  2     property: 'hello',  3 
 4     method: function() {  5         console.log(this.property);  6  }  7 };  8 
 9 var func = obj.method; 10 func();

  像上述这种状况,最终会输出undefined,由于当你将方法赋值给func变量时,func中就只有这个代码段的空壳而已,调用它就像调用一个普通的函数,this指向的是全局变量。只有单独调用 obj.method(); 才不会出现上述状况。app

  为此,ECMA后来提出了一个解决方案:在一整段JavaScript代码的开头一行 'use strict'; ,让浏览器执行严格模式排除错误。但当你看到前面这句话时,就要立马反应过来我所使用的浏览器的版本是否能够执行严格模式了(假如你的浏览器版本不支持严格模式,那这一行代码只会被当成普通的字符串执行,不会有什么结果)。若是在上述的代码放入严格模式下执行,this会被JavaScript定向为undefined,接着抛出一个TypeError错误。

 

  2. 函数调用模式

  当函数不是对象的属性,也就是在通常状况下,咱们像上面举例的代码同样直接声明的一个函数。此时,该函数中this的值指向全局对象。这种调用方法是最简单直接的方法:

1 var sub = function(a, b) { 2     return a - b; 3 }

  但须要注意的是,因为语言设计的失误,一个函数的内部函数this的值,本应该为这个函数this的值,而真实状况是它却指向了全局对象,所以,咱们须要更机智地提供一种解决方法:

 1 var motherLyn = {  2     generation: 'mother',  3     name: 'Lyn',  4     getFullName: function() {  5         var that = this;  6 
 7         var getGeneration = function() {  8             return that.generation;  9  } 10 
11         var getName = function() { 12             return that.name; 13  } 14 
15         return getGeneration() + getName(); 16  } 17 }

  若是咱们缺乏了第五行的代码,因为getGeneration和getName两个函数处于getFullName函数的内部,它们的this会指向window对象(全局对象),而window对象中没有generation和name属性,将会返回undefined。而在getFullName函数中声明一个that变量并让它指向this,避免了在内部函数中使用this,才能让代码向咱们指望的方向运行。

  还有一种解决方案,就是使用ES6中的箭头函数:

 1 var motherLyn = {  2     generation: 'mother',  3     name: 'Lyn',  4     getFullName: function() {  5         var getGeneration = () => {  6             return this.generation;  7  };  8         var getName = () => {  9             return this.name; 10  } 11 
12         return getGeneration() + getName(); 13  } 14 }

  ES6中,箭头函数能够取代内部函数调用,它的出现正是为了修正内部函数this指引不正确的问题。

 

  3. 构造器调用模式

  使用这种方式时,咱们务必要把函数名字的首字母大写,以与函数调用方式区分开来,每当看到首字母大写的函数就会本能地加上new关键字。这也是你们的一种约定,使咱们不会由于疏忽而调用时忘记添加new关键字,增长测试工做:

1 var MotherLyn = function(generation, name) { 2     this.generation = generation; 3     this.name = name; 4 }; 5 
6 var person = new MotherLyn("mother", "Lyn");

  这时候this仍然指向全局变量,只有使用new关键字时this才会指向函数对象自己,JavaScript也会提示此构造函数可能会转换为类声明。在能够不使用new的状况下,咱们能够尽可能不使用这种形式的构造器,由于当发生错误时,既没有编译时警告,也没有运行时警告。

  在之后关于对象建立的讲解中咱们将看到多种建立对象的方式,也包括彻底不使用new的建立方法,咱们须要结合不一样状况使用。

  在之后关于原型的讲解中咱们会看到一个对象的实例、它的构造器和它的原型三者之间的关系,这很是重要。

 

  4. Apply调用模式

  前面提到,函数本质上就是对象,所以函数是能够具备方法的。例如使用Function.apply方法,咱们能够重定义某个方法内this的值,以数组的形式传递指望传入的参数。这样哪怕一个对象没有继承另外一个对象,也可使用它里面的方法:

1 var myArray = [5, 6]; 2 var addArray = sum.apply(null, myArray); //调用到文章首部的sum函数,结果值为11
3 
4 var myObj = { 5     generation: 'my', 6     name: 'Obj'
7 }; 8 var getObjName = motherLyn.getFullName.apply(myObj); 9 //调用到上面的motherLyn对象中的getFullName方法,结果输出myObj

  咱们发现,哪怕上述的myObj并无继承自motherLyn对象,它仍然能经过apply方法,重定义this的值,重用其中的方法。

 

  注:上面咱们用到了apply方法以数组的形式重定义了arguments参数,但实际上因为语言设计的失误,arguments参数并非一个数组,而是一个array-like对象。也就是说,它除了有一个length属性之外,没有Array.prototype中的像concat这样的其余方法。

 

3、 做用域与闭包

  1. 做用域

  在编程语言中,做用域控制变量的可见性、生命周期、名称冲突和内存管理,对于程序员来讲是一项重要的服务。尽管像其余类C语法的语言同样,JavaScript也拥有函数做用域,但是直到ES5标准却一直没有块级做用域。这一点也是设计上比较糟糕的地方:

1 for(var i = 0; i < 5; i++) { 2  console.log(i); 3 }; 4 
5 console.log(i);

  像以上的代码会输出从0到5的六个i,缘由是由于JavaScript缺乏块级做用域,i的确从for语句中被泄露出来了。为此在ES6标准中let和const两种声明变量的方式被提出了(固然这两个关键字还会解决不少其余问题),这一点咱们会放到后续的进阶知识中讲解到。像以上这个代码使用let取代var保证了i不会被泄露,并且i不会被声明为window对象的属性,有效避免了污染全局对象的问题。

  在不少现代语言中,咱们更加推荐延迟声明变量。但在JavaScript中,因为缺乏块级做用域,尽管使用var声明变量还会获得变量提高(先使用再声明也是能够的),但延迟声明变量可能会编写出混乱的难以维护的代码。所以咱们仍是要在函数体的顶部将全部须要使用到的变量所有声明出来。

  2. 闭包

  所谓闭包,就是能够访问被它被建立时所处的上下文环境的函数。闭包支持了JavaScript实现更灵活更有逻辑性的表达方式,先前咱们提到这么屡次“因为设计失误”,如今咱们终于能够夸奖一次“设计很是精彩”了。闭包最多见的用法就是返回一个函数:

1 var getMe = function() { 2     var name = 'MotherLyn'; 3     var displayName = function() { 4  console.log(name); 5  } 6     return displayName; 7 }

  上述例子中的displayName函数就是典型的一个闭包,它能够得到它被建立时上下文环境(也就是getMe函数)中的变量,这些变量将持续地保留直至内部函数再也不须要使用(固然这必定程度上也会影响性能)。当咱们须要调用这个displayName函数,咱们这样来写:

1 var me = getMe(); 2 me();

  第一句调用到了getMe函数,将它的返回值给到了内部函数,并赋值给了me,此时name值已经肯定好了。最后调用到me函数来调用displayName函数。观察以上的函数,或许咱们能够考虑下闭包的做用:

  ① 在DOM操做中,咱们的代码一般是做为用户行为的回调函数执行,也就是说为了响应用户的某些行为而存在。所以在编写可复用的web代码时,闭包具备重要意义:

1 <a href="#" id="red">Red</a>
2 <a href="#" id="green">Green</a>
3 <a href="#" id="blue">Blue</a>
 1 function changeColor(color) {  2     return function() {  3         document.body.style.backgroundColor = color;  4  }  5 }  6 
 7 var change2Red = changeColor('red');  8 var change2Green = changeColor('green');  9 var change2Blue = changeColor('blue'); 10 
11 document.getElementById('red').onclick = change2Red; 12 document.getElementById('green').onclick = change2Green; 13 document.getElementById('blue').onclick = chage2Blue ;

  上面的代码跟面向对象的代码有点相似,它先创建了一个改换背景颜色的模板函数,经过赋不一样的值建立不一样的函数对象进行应用。

  ② 数据隐藏和封装(这也是模块化编程的基础):

 1 var motherLyn = function(generation, name) {  2     var myGeneration = generation;  3     var myName = name;  4     var str = '';  5     return {  6         getFullName: function() {  7             return str + myGeneration + myName;  8  }  9  } 10 } 11 
12 var me = motherLyn('mother', 'Lyn'); 13 
14 console.log(me.getFullName())

  在这个例子中,motherLyn做为一个构造函数,返回一个对象,咱们不能使用new关键字建立实例,所以函数名咱们采用了小写开头。创造了一个me实例之后,没法直接访问myGeneration、myName和str三个变量,只能获得getFullName函数的返回值,这样的封装效果就很是强了。因为JavaScript中内部函数的生命周期比它的外部函数要长,咱们利用这一点模仿了面向对象的私有对象。

  ③ 设计失误(for循环闭包详解)

  JavaScript中有一个很是常见的关于闭包的设计失误,当咱们在for循环中加入一层闭包,将会出现意外的结果:

1 <p>1</p>
2 <p>2</p>
3 <p>3</p>
4 <p>4</p>
1 var pList = document.querySelectorAll('p'); 2 
3 for(var i = 0; i < pList.length; i++) { 4     pList[i].onclick = function() { 5  console.log(i); 6  } 7 }

  观察代码,咱们获取到HTML中的全部四个p结点,做为一个结点数组。咱们指望循环这个数组,让其每个结点被点击时输出它在数组中的位置。但不幸的是,结果是每一次都输出for循环结束之后i的值。也就是在以上的例子中会永远输出4。缘由是内部函数实际上访问外部函数的实际变量而非它的复制,对于这个问题,咱们有许多种解决方案,最简单的莫过于:

1 for(let i = 0; i < pList.length; i++) { 2     pList[i].onclick = function() { 3  console.log(i); 4  } 5 }

  使用ES6标准中的let取代var,使i的做用域变为块级做用域,这样闭包中访问到的i也被修正为循环过程当中的i。咱们也能够建立一个辅助函数,让这个辅助函数返回绑定了当前i值的函数

1 var helper = function(i) { 2     return function() { 3  console.log(i); 4  } 5 } 6 
7 for(var i = 0; i < pList.length; i++) { 8     pList[i].onclick = helper(i); 9 }

  固然还有其余的方法,咱们比较不推荐的是将i绑定到循环当前的对象中做为一个属性存在,这样会污染当前的对象。

 

4、 箭头函数

  在常人的理解里,在内部函数中,this的指向应该是跟随它的外部函数的。在讲述函数调用模式时咱们发现了这个问题,因为JavaScript的设计失误,内部函数的this竟然指向全局对象。咱们只有使用这种hack写法,声明一个that变量指向跟this指向同样的内容来做为修正:

 1 var obj = {  2     name: 'an object',  3 
 4     oldExpression: function() {  5         var that = this;  6         var intervalFunction = function() {  7             return that.name;  8  }  9  } 10 }

  为了修正这一问题,箭头函数被提出,用以取代普通的内部函数写法了:

1 var obj = { 2     name: 'an object', 3 
4     newExpression: function() { 5         var intervalFunction = () => { 6             return this.name; 7  } 8  } 9 }

  箭头函数的语法为: () => {code} ,括号内填入参数,大括号内填入内部函数的全部内容,内部的this被修正为外部函数的this。常见的用法例如:

1 let array = [5, 2, 3, 8, 1, 6, 4]; 2 
3 // 从小到大排序
4 array.sort((x, y) => {return x - y;});

 

 

总结:1. 在JavaScript中函数也是一个对象,它的原型被链接到Function.prototype,再链接到Object.prototype;

   2. 函数有四种调用模式,分别是:① 方法调用模式, ② 函数调用模式, ③ 构造器调用模式, ④ Apply调用模式;

   3. 为了合理运用函数,咱们须要掌握两个要点,分别是① 做用域(ES5中只有函数做用域没有块做用域),② 闭包(尤为是for循环闭包的解决方案例子)。

   4. 箭头函数的使用。

原文出处:https://www.cnblogs.com/BlogOfMotherLyn/p/11266530.html

相关文章
相关标签/搜索