函數:菜鳥收割者
綜觀JavaScript中所有必須掌握的重點知識中,函數是我們在初學的時候最容易忽略的一個知識點。在學習的過程中,可能會有很多人、很多文章告訴你物件導向很重要,原型很重要,可是卻很少有人告訴你,物件導向中所有的重點難點,幾乎都與函數息息相關。
包括我之前幾篇文章介紹的執行上下文,變數對象,閉包,this等,都是圍繞函數來展開。
我知道很多人在學習中,很急切的希望自己快一點開始學習面向對象,學習模組,學習流行框架,然後迅速成為高手。但是我可以很負責的告訴你,關於函數的這些基礎東西沒理解到一定程度,那麼你的學習進度一定是舉步維艱的。
所以,大家一定要重視函數!
一、函數宣告、函數表達式、匿名函數與自執行函數
關於函數在實際開發中的應用,大體可以總結為函數宣告、函數表達式、匿名函數、自執行函數。
函數宣告
我們知道,JavaScript中,有兩種宣告方式,一個是使用var
的變數聲明,另一個是使用function
的函數宣告。
在前端基礎進階(三):變數物件詳解中我有提到過,在變數物件的建立過程中,函數宣告比變數宣告有更優先的執行順序,也就是我們常常提到的函數宣告提前。因此我們在執行上下文中,無論在什麼位置聲明了函數,我們都可以在同一個執行上下文中直接使用該函數。
fn(); // function function fn() { console.log('function'); }
函數表達式
與函數宣告不同,函數表達式使用了var進行聲明,那麼我們在確認他是否可以正確使用的時候就必須依照var的規則進行判斷,即變數宣告。我們知道使用var進行變數聲明,其實是進行了兩步驟操作。
// 变量声明 var a = 20; // 实际执行顺序 var a = undefined; // 变量声明,初始值undefined,变量提升,提升顺序次于function声明 a = 20; // 变量赋值,该操作不会提升
同樣的道理,當我們使用變數宣告的方式來宣告函數###時,就是我們常常說的函數表達式。函數表達的提升方式與變數宣告一致。
fn(); // 报错 var fn = function() { console.log('function'); }上範例的執行順序為:
var fn = undefined; // 变量声明提升 fn(); // 执行报错 fn = function() { // 赋值操作,此时将后边函数的引用赋值给fn console.log('function'); }
因此,由於宣告方式的不同,導致了函數宣告與函數表達式在使用上的一些差異需要我們注意,除此之外,這兩種形式的函數在使用上並無不同。關於上面例子中,函數表達式中的賦值操作,在其他一些地方也會被經常使用,我們清楚其中的關係即可。
在构造函数中添加方法 function Person(name) { this.name = name; this.age = age; // 在构造函数内部中添加方法 this.getAge = function() { return this.age; } this. } // 给原型添加方法 Person.prototype.getName = function() { return this.name; } // 在对象中添加方法 var a = { m: 20, getM: function() { return this.m; } }
匿名函數
在上面我們大概講述了函數表達式中的賦值運算。而匿名函數,顧名思義,就是指的沒有被顯示進行賦值運算的函數。它的使用場景,多作為一個參數傳入另一個函數。var a = 10; var fn = function(bar, num) { return bar() + num; } fn(function() { return a; }, 20)在上面的範例中,fn的第一個參數傳入了一個匿名函數。雖然該匿名函數沒有顯示的進行賦值操作,我們沒有辦法在外部執行上下文中引用到它,但是在fn函數內部,我們將該匿名函數賦值給了變數bar,保存在了fn變數物件的arguments物件中。
// 变量对象在fn上下文执行过程中的创建阶段 VO(fn) = { arguments: { bar: undefined, num: undefined, length: 2 } } // 变量对象在fn上下文执行过程中的执行阶段 // 变量对象变为活动对象,并完成赋值操作与执行可执行代码 VO -> AO AO(fn) = { arguments: { bar: function() { return a }, num: 20, length: 2 } }由於匿名函數傳入另一個函數之後,最終會在另一個函數中執行,因此我們也常常稱這個匿名函數為
#回呼函數#。關於匿名函數更多的內容,我會在下一篇深入探討柯里化的文章中進行更加詳細講解。
匿名函數的這個應用場景幾乎承擔了函數的所有難以理解的知識點,因此我們一定要對它的這些細節了解的足夠清楚,如果對於變量對象的演變過程你還看不太明白,一定要回過頭去看這篇文章:前端基礎進階(三):變數物件詳解
函數自執行與區塊級作用域
在ES5中,沒有區塊級作用域,因此我們常常使用函數自執行的方式來模仿區塊級作用域,這樣就提供了一個獨立的執行上下文,結合閉包,就為模組化提供了基礎。
(function() { // ... })();
一个模块往往可以包括:私有变量、私有方法、公有变量、公有方法。
根据作用域链的单向访问,外面可能很容易知道在这个独立的模块中,外部执行环境是无法访问内部的任何变量与方法的,因此我们可以很容易的创建属于这个模块的私有变量与私有方法。
(function() { // 私有变量 var age = 20; var name = 'Tom'; // 私有方法 function getName() { return `your name is ` + name; } })();
但是共有方法和变量应该怎么办?大家还记得我们前面讲到过的闭包的特性吗?没错,利用闭包,我们可以访问到执行上下文内部的变量和方法,因此,我们只需要根据闭包的定义,创建一个闭包,将你认为需要公开的变量和方法开放出来即可。
如果你对闭包了解不够,前端基础进阶(四):详细图解作用域链与闭包应该可以帮到你。
(function() { // 私有变量 var age = 20; var name = 'Tom'; // 私有方法 function getName() { return `your name is ` + name; } // 共有方法 function getAge() { return age; } // 将引用保存在外部执行环境的变量中,形成闭包,防止该执行环境被垃圾回收 window.getAge = getAge; })();
当然,闭包在模块中的重要作用,我们也在讲解闭包的时候已经强调过,但是这个知识点真的太重要,需要我们反复理解并且彻底掌握,因此为了帮助大家进一步理解闭包,我们来看看jQuery中,是如何利用我们模块与闭包的。
// 使用函数自执行的方式创建模块 (function(window, undefined) { // 声明jQuery构造函数 var jQuery = function(name) { // 主动在构造函数中,返回一个jQuery实例 return new jQuery.fn.init(name); } // 添加原型方法 jQuery.prototype = jQuery.fn = { constructor: jQuery, init:function() { ... }, css: function() { ... } } jQuery.fn.init.prototype = jQuery.fn; // 将jQuery改名为$,并将引用保存在window上,形成闭包,对外开发jQuery构造函数,这样我们就可以访问所有挂载在jQuery原型上的方法了 window.jQuery = window.$ = jQuery; })(window); // 在使用时,我们直接执行了构造函数,因为在jQuery的构造函数中通过一些手段,返回的是jQuery的实例,所以我们就不用再每次用的时候在自己new了 $('#p1');
在这里,我们只需要看懂闭包与模块的部分就行了,至于内部的原型链是如何绕的,为什么会这样写,我在讲面向对象的时候会为大家慢慢分析。举这个例子的目的所在,就是希望大家能够重视函数,因为在实际开发中,它无处不在。
接下来我要分享一个高级的,非常有用的模块的应用。当我们的项目越来越大,那么需要保存的数据与状态就越来越多,因此,我们需要一个专门的模块来维护这些数据,这个时候,有一个叫做状态管理器的东西就应运而生。对于状态管理器,最出名的,我想非redux莫属了。虽然对于还在学习中的大家来说,redux是一个有点高深莫测的东西,但是在我们学习之前,可以先通过简单的方式,让大家大致了解状态管理器的实现原理,为我们未来的学习奠定坚实的基础。
先来直接看代码。
// 自执行创建模块 (function() { // states 结构预览 // states = { // a: 1, // b: 2, // m: 30, // o: {} // } var states = {}; // 私有变量,用来存储状态与数据 // 判断数据类型 function type(elem) { if(elem == null) { return elem + ''; } return toString.call(elem).replace(/[\[\]]/g, '').split(' ')[1].toLowerCase(); } /** * @Param name 属性名 * @Description 通过属性名获取保存在states中的值 */ function get(name) { return states[name] ? states[name] : ''; } function getStates() { return states; } /* * @param options {object} 键值对 * @param target {object} 属性值为对象的属性,只在函数实现时递归中传入 * @desc 通过传入键值对的方式修改state树,使用方式与小程序的data或者react中的setStates类似 */ function set(options, target) { var keys = Object.keys(options); var o = target ? target : states; keys.map(function(item) { if(typeof o[item] == 'undefined') { o[item] = options[item]; } else { type(o[item]) == 'object' ? set(options[item], o[item]) : o[item] = options[item]; } return item; }) } // 对外提供接口 window.get = get; window.set = set; window.getStates = getStates; })() // 具体使用如下 set({ a: 20 }); // 保存 属性a set({ b: 100 }); // 保存属性b set({ c: 10 }); // 保存属性c // 保存属性o, 它的值为一个对象 set({ o: { m: 10, n: 20 } }) // 修改对象o 的m值 set({ o: { m: 1000 } }) // 给对象o中增加一个c属性 set({ o: { c: 100 } }) console.log(getStates())
demo实例在线地址
我之所以说这是一个高级应用,是因为在单页应用中,我们很可能会用到这样的思路。根据我们提到过的知识,理解这个例子其实很简单,其中的难点估计就在于set方法的处理上,因为为了具有更多的适用性,因此做了很多适配,用到了递归等知识。如果你暂时看不懂,没有关系,知道如何使用就行了,上面的代码可以直接运用于实际开发。记住,当你需要保存的状态太多的时候,你就想到这一段代码就行了。
函数自执行的方式另外还有其他几种写法,诸如
!function(){}()
,+function(){}()
二、函数参数传递方式:按值传递
还记得基本数据类型与引用数据类型在复制上的差异吗?基本数据类型复制,是直接值发生了复制,因此改变后,各自相互不影响。但是引用数据类型的复制,是保存在变量对象中的引用发生了复制,因此复制之后的这两个引用实际访问的实际是同一个堆内存中的值。当改变其中一个时,另外一个自然也被改变。如下例。
var a = 20; var b = a; b = 10; console.log(a); // 20 var m = { a: 1, b: 2 } var n = m; n.a = 5; console.log(m.a) // 5
当值作为函数的参数传递进入函数内部时,也有同样的差异。我们知道,函数的参数在进入函数后,实际是被保存在了函数的变量对象中,因此,这个时候相当于发生了一次复制。如下例。
var a = 20; function fn(a) { a = a + 10; return a; } console.log(a); // 20
var a = { m: 10, n: 20 } function fn(a) { a.m = 20; return a; } fn(a); console.log(a); // { m: 20, n: 20 }
正是由于这样的不同,导致了许多人在理解函数参数的传递方式时,就有许多困惑。到底是按值传递还是按引用传递?实际上结论仍然是按值传递,只不过当我们期望传递一个引用类型时,真正传递的,只是这个引用类型保存在变量对象中的引用而已。为了说明这个问题,我们看看下面这个例子。
var person = { name: 'Nicholas', age: 20 } function setName(obj) { // 传入一个引用 obj = {}; // 将传入的引用指向另外的值 obj.name = 'Greg'; // 修改引用的name属性 } setName(person); console.log(person.name); // Nicholas 未被改变
在上面的例子中,如果person是按引用传递,那么person就会自动被修改为指向其name属性值为Gerg的新对象。但是我们从结果中看到,person对象并未发生任何改变,因此只是在函数内部引用被修改而已。
四、函数式编程
虽然JavaScript并不是一门纯函数式编程的语言,但是它使用了许多函数式编程的特性。因此了解这些特性可以让我们更加了解自己写的代码。
函数是第一等公民
所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。这些场景,我们应该见过很多。
var a = function foo() {} // 赋值 function fn(function() {}, num) {} // 函数作为参数 // 函数作为返回值 function var() { return function() { ... ... } }
只用"表达式",不用"语句"
"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
了解这一点,可以让我们自己在封装函数的时候养成良好的习惯。借助这个特性,我们在学习其他API的时候,了解函数的返回值也是一个十分重要的习惯。
没有"副作用"
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
即所谓的只要是同样的参数传入,返回的结果一定是相等的。
闭包
闭包是函数式编程语言的重要特性,我也在前面几篇文章中说了很多关于闭包的内容。这里不再赘述。
柯里化
理解柯里化稍微有点难,我在下一篇文章里专门单独来深入分析。
五、函数封装
在我们自己封装函数时,最好尽量根据函数式编程的特点来编写。当然在许多情况下并不能完全做到,比如函数中我们常常会利用模块中的私有变量等。
普通封装
function add(num1, num2) { return num1 + num2; } add(20, 10); // 30
挂载在对象上
if(typeof Array.prototype.add !== 'function') { Array.prototype.add = function() { var i = 0, len = this.length, result = 0; for( ; i <p>修改<a href="http://www.php.cn/wiki/58.html" target="_blank">数组</a>对象的例子,常在面试中被问到类似的,但是并不建议在实际开发中扩展原生对象。与普通封装不一样的是,因为挂载在对象的原型上我们可以通过this来<a href="http://www.php.cn/code/634.html" target="_blank">访问对象的属性和方法</a>,所以这种封装在实际使用时会有许多的难点,因此我们一定要掌握好this。</p><p><br></p>
以上是前端進階(七):函數與函數式編程的詳細內容。更多資訊請關注PHP中文網其他相關文章!

JavaScript字符串替換方法詳解及常見問題解答 本文將探討兩種在JavaScript中替換字符串字符的方法:在JavaScript代碼內部替換和在網頁HTML內部替換。 在JavaScript代碼內部替換字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 該方法僅替換第一個匹配項。要替換所有匹配項,需使用正則表達式並添加全局標誌g: str = str.replace(/fi

本文討論了在瀏覽器中優化JavaScript性能的策略,重點是減少執行時間並最大程度地減少對頁面負載速度的影響。

本文討論了使用瀏覽器開發人員工具的有效JavaScript調試,專注於設置斷點,使用控制台和分析性能。

將矩陣電影特效帶入你的網頁!這是一個基於著名電影《黑客帝國》的酷炫jQuery插件。該插件模擬了電影中經典的綠色字符特效,只需選擇一張圖片,插件就會將其轉換為充滿數字字符的矩陣風格畫面。快來試試吧,非常有趣! 工作原理 插件將圖片加載到畫布上,讀取像素和顏色值: data = ctx.getImageData(x, y, settings.grainSize, settings.grainSize).data 插件巧妙地讀取圖片的矩形區域,並利用jQuery計算每個區域的平均顏色。然後,使用

本文將引導您使用jQuery庫創建一個簡單的圖片輪播。我們將使用bxSlider庫,它基於jQuery構建,並提供許多配置選項來設置輪播。 如今,圖片輪播已成為網站必備功能——一圖胜千言! 決定使用圖片輪播後,下一個問題是如何創建它。首先,您需要收集高質量、高分辨率的圖片。 接下來,您需要使用HTML和一些JavaScript代碼來創建圖片輪播。網絡上有很多庫可以幫助您以不同的方式創建輪播。我們將使用開源的bxSlider庫。 bxSlider庫支持響應式設計,因此使用此庫構建的輪播可以適應任何

數據集對於構建API模型和各種業務流程至關重要。這就是為什麼導入和導出CSV是經常需要的功能。在本教程中,您將學習如何在Angular中下載和導入CSV文件

核心要点 利用 JavaScript 增强结构化标记可以显著提升网页内容的可访问性和可维护性,同时减小文件大小。 JavaScript 可有效地用于为 HTML 元素动态添加功能,例如使用 cite 属性自动在块引用中插入引用链接。 将 JavaScript 与结构化标记集成,可以创建动态用户界面,例如无需页面刷新的选项卡面板。 确保 JavaScript 增强功能不会妨碍网页的基本功能至关重要;即使禁用 JavaScript,页面也应保持功能正常。 可以使用高级 JavaScript 技术(


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

ZendStudio 13.5.1 Mac
強大的PHP整合開發環境

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能

MantisBT
Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SublimeText3 Linux新版
SublimeText3 Linux最新版

mPDF
mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),