首頁 > web前端 > js教程 > 主體

閉包有話說 - 大前端

高洛峰
發布: 2017-02-08 17:57:48
原創
993 人瀏覽過

引言

剛學習前端的時候,看到閉包這個詞,總是一臉懵逼,面試的時候,問到這個問題,也是回答的含含糊糊,總感覺有層隔膜,覺得這個概念很神奇,如果能掌握,必將功力大漲。其實,閉包沒有這麼神秘,它無所不在。

一個簡短的問題

首先,來看一個問題。

請用一句話描述什麼是閉包,並寫出程式碼進行說明。

如果能毫不猶豫的說出來,並能給出解釋,那下面文字對你來說就沒有往下看的必要了。
就這個問題,結合我查閱的資料和經驗,在這裡簡單的說一下,如果哪裡有不對的,歡迎指正。

先回答上面的問題,什麼是閉包。

閉包是一個概念,它描述了函數執行完畢後,依然駐留記憶體的現象。

程式碼描述:

function foo() {

    var a = 2;

    function bar(){
        console.log(a);
    }

    return bar;
}

var test = foo();
test(); //2
登入後複製
登入後複製

上面這段程式碼,清晰的展示了閉包。

函數 bar() 的詞法作用域能夠存取 foo()的內部作用域。然後我們將bar()函數本身當作一個值類型來傳遞。上面這個例子,我們將bar() 所引用的函數物件本身當作回傳值。

foo() 執行完畢之後, 其內部作用域並沒有被銷毀,因為bar()依然保持著對內部作用域的引用,拜bar()的位置所賜,它擁有涵蓋foo()內部作用域的閉包,使得該作用域能夠一直存活,以供bar()在之後的任何時間進行引用。這個引用,其實就是閉包。
也正是這個原因,test被實際呼叫的時候,它可以存取定義時的詞法作用域,所以,才能存取到a.

函數傳遞也可以是間接的:

    var fn;
    function foo(){

        var a = 2;

        function baz() {
            console.log( a );
        }
        fn = baz; //将baz 分配给全局变量
    }

    function bar(){
        fn();
    }
    foo();
    bar(); //2
登入後複製
登入後複製

所以,無論透過何種手段將內部函數傳遞到其所在的詞法作用域外,它都會持有對原始定義作用域的引用。也就是說,無論在哪裡執行這個函數,都會使用閉包。也是這個原因,我們才可以很方便的使用回呼函數而不用關心其具體細節。

其實,在定時器,事件監聽器,ajax請求, 跨窗口通信,Web Workers 或者任何其他的同步 或 異步任務中,只要使用了回調函數,實際上就是在使用閉包。

到這裡,或許你已經對閉包有個大概的了解,下面我再舉幾個例子來幫你加深對閉包的認識。

幾個更具體的例子

首先,就先看一下所謂的立即執行函數.

var a = 2;

(function IIFE() { 
   console.log(a); 
 })();

//2
登入後複製
登入後複製

這個立即執行函數通常被認為是經典的閉包例子,它可以正常工作,但嚴格意義上講,它並不是閉包。
為什麼呢?

因為這個IIFE函數並不是在它本身的詞法作用域之外執行的。它在定義時所在的作用域中執行了。而且,變數a 是透過普通的詞法作用域來尋找的,而不是透過閉包。

另一個用來說明閉包的例子是循環。

    <p class="tabs">
        <li class="tab">some text1</li>
        <li class="tab">some text2</li>
        <li class="tab">some text3</li>
    </p>
登入後複製
登入後複製
var handler = function(nodes) {

    for(var i = 0, l = nodes.length; i < l ; i++) {
        
        nodes[i].onclick = function(){

            console.log(i);

        }
    }
}

var tabs = document.querySelectorAll('.tabs .tab');
    handler(tabs);
登入後複製
登入後複製

我們預期的結果是log  0 ,1,2;

執行之後的結果卻是是三個3;

這是為什麼呢?

先解釋下這個3是怎麼來的,

看一下循環體,循環的終止條件是  i 因此 ,輸出顯示的是循環結束時 i 的最終值。 根據作用域的工作原理,儘管循環中的函數是在各個迭代中分別定義的,但是它們都被封閉在一個共享的全局作用域中,因此實際上是只有一個i.

handler 函數的本意是想把唯一的i傳遞給事件處理器,但失敗了。
因為事件處理器函數綁定了i本身,而不是函數在構造時的i的值.

知道了這個之後,我們可以做出相應的調整:

var handler = function(nodes) {

    var helper = function(i){
        return function(e){
            console.log(i); // 0 1 2
        }
    }

    for(var i = 0, l = nodes.length; i < l ; i++) {
        
        nodes[i].onclick = helper(i);
    }
}
登入後複製

在循環外創建一個輔助函數,讓這個輔助函數在傳回一個綁定了目前i的值的函數,這樣就不會混淆了。

明白了這一點,就會發現,上面的處理就是為了創建一個新的作用域,換句話說,每次迭代我們都需要一個塊作用域.

說到塊作用域,就不得不提一個字,那就是let.

所以,如果你不想過多的使用閉包,就可以使用let:

var handler = function(nodes) {

    for(let i = 0, l = nodes.length; i < l ; i++) {
        
        //nodes[i].index = i;

        nodes[i].onclick = function(){

            console.log(i); // 0 1 2


        }
    }
}
登入後複製
登入後複製

jQuery中的閉包

先來看個例子

     var sel = $("#con"); 
     setTimeout( function (){ 
         sel.css({background:"gray"}); 
     }, 2000);
登入後複製
登入後複製

上邊的程式碼使用了jQuery的選擇器,找到id 為con 的元素,註冊計時器,兩秒之後,將背景色設為灰色。

這個程式碼片段的神奇之處在於,在呼叫了 setTimeout 函數之後,con 依舊被保持在函數內部,當兩秒鐘之後,id 為 con 的 p 元素的背景色確實得到了改變。應該注意的是,setTimeout 在呼叫之後已經回傳了,但是 con 沒有被釋放,這是因為 con 引用了全域作用域裡的變數 con。

以上的例子幫助我們了解了更多關於閉包的細節,下面我們就深入閉包世界探尋一番。

深入理解闭包

首先看一个概念-执行上下文(Execution Context)。

执行上下文是一个抽象的概念,ECMAScript 规范使用它来追踪代码的执行。它可能是你的代码第一次执行或执行的流程进入函数主体时所在的全局上下文。

闭包有话说 - 大前端

在任意一个时间点,只能有唯一一个执行上下文在运行之中。

这就是为什么 JavaScript 是“单线程”的原因,意思就是一次只能处理一个请求。

一般来说,浏览器会用栈来保存这个执行上下文。

栈是一种“后进先出” (Last In First Out) 的数据结构,即最后插入该栈的元素会最先从栈中被弹出(这是因为我们只能从栈的顶部插入或删除元素)。

当前的执行上下文,或者说正在运行中的执行上下文永远在栈顶。

当运行中的上下文被完全执行以后,它会由栈顶弹出,使得下一个栈顶的项接替它成为正在运行的执行上下文。

除此之外,一个执行上下文正在运行并不代表另一个执行上下文需要等待它完成运行之后才可以开始运行。

有时会出现这样的情况,一个正在运行中的上下文暂停或中止,另外一个上下文开始执行。暂停的上下文可能在稍后某一时间点从它中止的位置继续执行。

一个新的执行上下文被创建并推入栈顶,成为当前的执行上下文,这就是执行上下文替代的机制。

闭包有话说 - 大前端

当我们有很多执行上下文一个接一个地运行时——通常情况下会在中间暂停然后再恢复运行——为了能很好地管理这些上下文的顺序和执行情况,我们需要用一些方法来对其状态进行追踪。而实际上也是如此,根据ECMAScript的规范,每个执行上下文都有用于跟踪代码执行进程的各种状态的组件。包括:

  • 代码执行状态:任何需要开始运行,暂停和恢复执行上下文相关代码执行的状态
     函数:上下文中正在执行的函数对象(正在执行的上下文是脚本或模块的情况下可能是null)

  • Realm:一系列内部对象,一个ECMAScript全局环境,所有在全局环境的作用域内加载的ECMAScript代码,和其他相关的状态及资源。

  • 词法环境:用于解决此执行上下文内代码所做的标识符引用。

  • 变量环境:一种词法环境,该词法环境的环境记录保留了变量声明时在执行上下文中创建的绑定关系。

模块与闭包

现在的开发都离不开模块化,下面说说模块是如何利用闭包的。

先看一个实际中的例子。
这是一个统计模块,看一下代码:

    define("components/webTrends", ["webTrendCore"], function(require,exports, module) {
    
    
        var webTrendCore = require("webTrendCore");  
        var webTrends = {
             init:function (obj) {
                 var self = this;
                self.dcsGetId();
                self.dcsCollect();
            },
    
             dcsGetId:function(){
                if (typeof(_tag) != "undefined") {
                 _tag.dcsid="dcs5w0txb10000wocrvqy1nqm_6n1p";
                 _tag.dcsGetId();
                }
            },
    
            dcsCollect:function(){
                 if (typeof(_tag) != "undefined") {
                    _tag.DCSext.platform="weimendian";
                    if(document.readyState!="complete"){
                    document.onreadystatechange = function(){
                        if(document.readyState=="complete") _tag.dcsCollect()
                        }
                    }
                    else _tag.dcsCollect()
                }
            }
    
        };
    
      module.exports = webTrends;
    
    })
登入後複製
登入後複製

在主页面使用的时候,调用一下就可以了:

var webTrends = require("webTrends");
webTrends.init();
登入後複製
登入後複製

在定义的模块中,我们暴露了webTrends对象,在外面调用返回对象中的方法就形成了闭包。

模块的两个必要条件:

  • 必须有外部的封闭函数,该函数必须至少被调用一次

  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

性能考量

如果一个任务不需要使用闭包,那最好不要在函数内创建函数。
原因很明显,这会 拖慢脚本的处理速度,加大内存消耗 。

举个例子,当需要创建一个对象时,方法通常应该和对象的原型关联,而不是定义到对象的构造函数中。 原因是 每次构造函数被调用, 方法都会被重新赋值 (即 对于每个对象创建),这显然是一种不好的做法。

看一个能说明问题,但是不推荐的做法:

    function MyObject(name, message) {
    
      this.name = name.toString();
      this.message = message.toString();
      
      this.getName = function() {
        return this.name;
      };
    
      this.getMessage = function() {
        return this.message;
      };
    }
登入後複製
登入後複製

上面的代码并没有很好的利用闭包,我们来改进一下:

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
    }
    
    MyObject.prototype = {
      getName: function() {
        return this.name;
      },
      getMessage: function() {
        return this.message;
      }
    };
登入後複製
登入後複製

好一些了,但是不推荐重新定义原型,再来改进下:

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}

MyObject.prototype.getName = function() {
       return this.name;
};

MyObject.prototype.getMessage = function() {
   return this.message;
};
登入後複製
登入後複製

很显然,在现有的原型上添加方法是一种更好的做法。

上面的代码还可以写的更简练:

    function MyObject(name, message) {
        this.name = name.toString();
        this.message = message.toString();
    }
    
    (function() {
        this.getName = function() {
            return this.name;
        };
        this.getMessage = function() {
            return this.message;
        };
    }).call(MyObject.prototype);
登入後複製
登入後複製

在前面的三个示例中,继承的原型可以由所有对象共享,并且在每个对象创建时不需要定义方法定义。如果想看更多细节,可以参考对象模型。

闭包的使用场景:

  • 使用闭包可以在JavaScript中模拟块级作用域;

  • 閉包可以用於在物件中建立私有變數。

閉包的優缺點

優點:

  • 邏輯連續,當閉包作為另一個函數調用的參數時,避免你脫離當前邏輯而單獨編寫額外邏輯。

  • 方便呼叫上下文的局部變數。

  • 加強封裝性,第2點的延伸,可以達到對變數的保護作用。

缺點:

  • 記憶體浪費。這個內存浪費不僅因為它常駐內存,對閉包的使用不當會造成無效內存的產生。

結語

前面對閉包做了一些簡單的解釋,最後再總結下,其實閉包沒什麼特別的,其特點是:

  • 函數巢狀函數

  • 函數內巢函數

函數內巢函數

函數內巢函數

函數內巢函數

訪問到外部的變數或物件

避免了垃圾回收

歡迎交流,以上;-)

參考資料

讓我們一起學習JavaScriptdoScript

包包

Closures

引言

剛學習前端的時候,看到閉包這個詞,總是一臉懵逼,面試的時候,問到這個問題,也是回答的含含糊糊,總包這個詞,總是一臉懵逼,面試的時候,問到這個問題,也是回答的含含糊糊,總包感覺有層隔膜,覺得這個概念很神奇,如果能掌握,必將功力大漲。其實,閉包沒有這麼神秘,它無所不在。


一個簡短的問題

首先,來看一個問題。

請用一句話描述什麼是閉包,並寫出程式碼進行說明。

如果能毫不猶豫的說出來,並能給出解釋,那下面文字對你來說就沒有往下看的必要了。
就這個問題,結合我查閱的資料和經驗,在這裡簡單的說一下,如果哪裡有不對的,歡迎指正。

先回答上面的問題,什麼是閉包。

閉包是一個概念,它描述了函數執行完畢後,依然駐留記憶體的現象。


程式碼描述:

function foo() {

    var a = 2;

    function bar(){
        console.log(a);
    }

    return bar;
}

var test = foo();
test(); //2
登入後複製
登入後複製

上面這段程式碼,清晰的展示了閉包。

函數 bar() 的詞法作用域能夠存取 foo()的內部作用域。然後我們將bar()函數本身當作一個值類型來傳遞。上面這個例子,我們將bar() 所引用的函數物件本身當作回傳值。

foo() 執行完畢之後, 其內部作用域並沒有被銷毀,因為bar()依然保持著對內部作用域的引用,拜bar()的位置所賜,它擁有涵蓋foo()內部作用域的閉包,使得該作用域能夠一直存活,以供bar()在之後的任何時間進行引用。這個引用,其實就是閉包。

也正是這個原因,test被實際呼叫的時候,它可以存取定義時的詞法作用域,所以,才能存取到a.

函數傳遞也可以是間接的:

    var fn;
    function foo(){

        var a = 2;

        function baz() {
            console.log( a );
        }
        fn = baz; //将baz 分配给全局变量
    }

    function bar(){
        fn();
    }
    foo();
    bar(); //2
登入後複製
登入後複製
所以,無論透過何種手段將內部函數傳遞到其所在的詞法作用域外,它都會持有對原始定義作用域的引用。也就是說,無論在哪裡執行這個函數,都會使用閉包。也是這個原因,我們才可以很方便的使用回呼函數而不用關心其具體細節。

其實,在定時器,事件監聽器,ajax請求, 跨窗口通信,Web Workers 或者任何其他的同步 或 異步任務中,只要使用了回調函數,實際上就是在使用閉包。

到這裡,或許你已經對閉包有個大概的了解,下面我再舉幾個例子來幫你加深對閉包的認識。

幾個更具體的例子

首先,就先看一下所謂的立即執行函數.🎜
var a = 2;

(function IIFE() { 
   console.log(a); 
 })();

//2
登入後複製
登入後複製
🎜這個立即執行函數通常被認為是經典的閉包例子,它可以正常工作,但嚴格意義上講,它並不是閉包。 🎜為什麼呢? 🎜🎜因為這個IIFE函數並不是在它本身的詞法作用域之外執行的。它在定義時所在的作用域中執行了。而且,變數a 是透過普通的詞法作用域來尋找的,而不是透過閉包。 🎜🎜另一個用來說明閉包的例子是循環。 🎜
    <p class="tabs">
        <li class="tab">some text1</li>
        <li class="tab">some text2</li>
        <li class="tab">some text3</li>
    </p>
登入後複製
登入後複製
var handler = function(nodes) {

    for(var i = 0, l = nodes.length; i < l ; i++) {
        
        nodes[i].onclick = function(){

            console.log(i);

        }
    }
}

var tabs = document.querySelectorAll('.tabs .tab');
    handler(tabs);
登入後複製
登入後複製
🎜我們預期的結果是log  0 ,1,2;🎜🎜執行之後的結果卻是是三個3;🎜🎜這是為什麼呢? 🎜🎜先解釋下這個3是怎麼來的,🎜🎜看一下循環體,循環的終止條件是  i var handler = function(nodes) {     var helper = function(i){         return function(e){             console.log(i); // 0 1 2         }     }     for(var i = 0, l = nodes.length; i < l ; i++) {                  nodes[i].onclick = helper(i);     } }

在循环外创建一个辅助函数,让这个辅助函数在返回一个绑定了当前i的值的函数,这样就不会混淆了。

明白了这点,就会发现,上面的处理就是为了创建一个新的作用域,换句话说,每次迭代我们都需要一个块作用域.

说到块作用域,就不得不提一个词,那就是let.

所以,如果你不想过多的使用闭包,就可以使用let:

var handler = function(nodes) {

    for(let i = 0, l = nodes.length; i < l ; i++) {
        
        //nodes[i].index = i;

        nodes[i].onclick = function(){

            console.log(i); // 0 1 2


        }
    }
}
登入後複製
登入後複製

jQuery中的闭包

先来看个例子

     var sel = $("#con"); 
     setTimeout( function (){ 
         sel.css({background:"gray"}); 
     }, 2000);
登入後複製
登入後複製

上边的代码使用了 jQuery 的选择器,找到 id 为 con 的元素,注册计时器,两秒之后,将背景色设置为灰色。

这个代码片段的神奇之处在于,在调用了 setTimeout 函数之后,con 依旧被保持在函数内部,当两秒钟之后,id 为 con 的 p 元素的背景色确实得到了改变。应该注意的是,setTimeout 在调用之后已经返回了,但是 con 没有被释放,这是因为 con 引用了全局作用域里的变量 con。

以上的例子帮助我们了解了更多关于闭包的细节,下面我们就深入闭包世界探寻一番。

深入理解闭包

首先看一个概念-执行上下文(Execution Context)。

执行上下文是一个抽象的概念,ECMAScript 规范使用它来追踪代码的执行。它可能是你的代码第一次执行或执行的流程进入函数主体时所在的全局上下文。

闭包有话说 - 大前端

在任意一个时间点,只能有唯一一个执行上下文在运行之中。

这就是为什么 JavaScript 是“单线程”的原因,意思就是一次只能处理一个请求。

一般来说,浏览器会用栈来保存这个执行上下文。

栈是一种“后进先出” (Last In First Out) 的数据结构,即最后插入该栈的元素会最先从栈中被弹出(这是因为我们只能从栈的顶部插入或删除元素)。

当前的执行上下文,或者说正在运行中的执行上下文永远在栈顶。

当运行中的上下文被完全执行以后,它会由栈顶弹出,使得下一个栈顶的项接替它成为正在运行的执行上下文。

除此之外,一个执行上下文正在运行并不代表另一个执行上下文需要等待它完成运行之后才可以开始运行。

有时会出现这样的情况,一个正在运行中的上下文暂停或中止,另外一个上下文开始执行。暂停的上下文可能在稍后某一时间点从它中止的位置继续执行。

一个新的执行上下文被创建并推入栈顶,成为当前的执行上下文,这就是执行上下文替代的机制。

闭包有话说 - 大前端

当我们有很多执行上下文一个接一个地运行时——通常情况下会在中间暂停然后再恢复运行——为了能很好地管理这些上下文的顺序和执行情况,我们需要用一些方法来对其状态进行追踪。而实际上也是如此,根据ECMAScript的规范,每个执行上下文都有用于跟踪代码执行进程的各种状态的组件。包括:

  • 代码执行状态:任何需要开始运行,暂停和恢复执行上下文相关代码执行的状态
     函数:上下文中正在执行的函数对象(正在执行的上下文是脚本或模块的情况下可能是null)

  • Realm:一系列内部对象,一个ECMAScript全局环境,所有在全局环境的作用域内加载的ECMAScript代码,和其他相关的状态及资源。

  • 词法环境:用于解决此执行上下文内代码所做的标识符引用。

  • 变量环境:一种词法环境,该词法环境的环境记录保留了变量声明时在执行上下文中创建的绑定关系。

模块与闭包

现在的开发都离不开模块化,下面说说模块是如何利用闭包的。

先看一个实际中的例子。
这是一个统计模块,看一下代码:

    define("components/webTrends", ["webTrendCore"], function(require,exports, module) {
    
    
        var webTrendCore = require("webTrendCore");  
        var webTrends = {
             init:function (obj) {
                 var self = this;
                self.dcsGetId();
                self.dcsCollect();
            },
    
             dcsGetId:function(){
                if (typeof(_tag) != "undefined") {
                 _tag.dcsid="dcs5w0txb10000wocrvqy1nqm_6n1p";
                 _tag.dcsGetId();
                }
            },
    
            dcsCollect:function(){
                 if (typeof(_tag) != "undefined") {
                    _tag.DCSext.platform="weimendian";
                    if(document.readyState!="complete"){
                    document.onreadystatechange = function(){
                        if(document.readyState=="complete") _tag.dcsCollect()
                        }
                    }
                    else _tag.dcsCollect()
                }
            }
    
        };
    
      module.exports = webTrends;
    
    })
登入後複製
登入後複製

在主页面使用的时候,调用一下就可以了:

var webTrends = require("webTrends");
webTrends.init();
登入後複製
登入後複製

在定义的模块中,我们暴露了webTrends对象,在外面调用返回对象中的方法就形成了闭包。

模块的两个必要条件:

  • 必须有外部的封闭函数,该函数必须至少被调用一次

  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

性能考量

如果一个任务不需要使用闭包,那最好不要在函数内创建函数。
原因很明显,这会 拖慢脚本的处理速度,加大内存消耗 。

举个例子,当需要创建一个对象时,方法通常应该和对象的原型关联,而不是定义到对象的构造函数中。 原因是 每次构造函数被调用, 方法都会被重新赋值 (即 对于每个对象创建),这显然是一种不好的做法。

看一个能说明问题,但是不推荐的做法:

    function MyObject(name, message) {
    
      this.name = name.toString();
      this.message = message.toString();
      
      this.getName = function() {
        return this.name;
      };
    
      this.getMessage = function() {
        return this.message;
      };
    }
登入後複製
登入後複製

上面的代码并没有很好的利用闭包,我们来改进一下:

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
    }
    
    MyObject.prototype = {
      getName: function() {
        return this.name;
      },
      getMessage: function() {
        return this.message;
      }
    };
登入後複製
登入後複製

好一些了,但是不推荐重新定义原型,再来改进下:

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}

MyObject.prototype.getName = function() {
       return this.name;
};

MyObject.prototype.getMessage = function() {
   return this.message;
};
登入後複製
登入後複製

很显然,在现有的原型上添加方法是一种更好的做法。

上面的代码还可以写的更简练:

    function MyObject(name, message) {
        this.name = name.toString();
        this.message = message.toString();
    }
    
    (function() {
        this.getName = function() {
            return this.name;
        };
        this.getMessage = function() {
            return this.message;
        };
    }).call(MyObject.prototype);
登入後複製
登入後複製

在前面的三个示例中,继承的原型可以由所有对象共享,并且在每个对象创建时不需要定义方法定义。如果想看更多细节,可以参考对象模型。

闭包的使用场景:

  • 使用闭包可以在JavaScript中模拟块级作用域;

  • 闭包可以用于在对象中创建私有变量。

闭包的优缺点

优点:

  • 逻辑连续,当闭包作为另一个函数调用的参数时,避免你脱离当前逻辑而单独编写额外逻辑。

  • 方便调用上下文的局部变量。

  • 加强封装性,第2点的延伸,可以达到对变量的保护作用。

缺点:

  • 内存浪费。这个内存浪费不仅仅因为它常驻内存,对闭包的使用不当会造成无效内存的产生。

结语

前面对闭包做了一些简单的解释,最后再总结下,其实闭包没什么特别的,其特点是:

  • 函数嵌套函数

  • 函数内部可以访问到外部的变量或者对象

  • 避免了垃圾回收

更多闭包有话说 - 大前端相关文章请关注PHP中文网!




相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!