LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

JavaScript是按順序執(zhí)行的嗎?聊聊JavaScript中的變量提升

freeflydom
2024年12月17日 15:53 本文熱度 738

作為一位前端開發(fā)者,我們經(jīng)常會聽到這么一句話:“JavaScript的執(zhí)行是按照順序自上而下依次執(zhí)行的。”這句話說的并沒有錯。但是它似乎又好像不完全對。我們先來看以下這段代碼。你覺得結(jié)果會輸出什么?

1 showName()
2 console.log(myName)
3 
4 var myName = '修謙'
5 function showName() {
6     console.log('我的名字叫修謙')
7 }

若是按照之前說的自上而下依次執(zhí)行的邏輯話,那么應(yīng)該輸出的結(jié)果應(yīng)該是:

1、因為函數(shù)showName執(zhí)行時,其并未定義,因此會報錯

2、同樣的,因為變量myName也并未定義,因此也是會報錯

然而當(dāng)我們在瀏覽器控制臺執(zhí)行的時候,其實際的結(jié)果卻如下圖所示。

 

代碼竟然沒有報錯!第一行輸出了“我的名字叫修謙”,第 2 行則輸出了“undefined”,這時候你是否會有疑問:“這怎么和前面想象中的順序執(zhí)行有點不一樣啊!怎么結(jié)果會是這樣的呢?”

到這里,我想你應(yīng)該想到了一點什么。那就是:“函數(shù)和變量是可以在定義之前使用的”但是我們?nèi)绻麍?zhí)行未定義的函數(shù)和變量的話,又會是一個什么樣的結(jié)果呢?

我們嘗試著將之前的第三行代碼刪掉,然后執(zhí)行。

1 showName()
2 console.log(myName)
3 
4 function showName() {
5     console.log('我的名字叫修謙')
6 }

運行代碼后,如下圖所示,這一次我們看到的結(jié)果是函數(shù)已經(jīng)執(zhí)行了,但是console函數(shù)輸出的已經(jīng)報錯了,輸出了“myName is not defined”

 

到這里,對于以上的兩個結(jié)果,你是否又能得到了一些新的啟示呢?事實上,通過上面的兩次代碼執(zhí)行,我們至少可以得到以下幾個結(jié)論:

1、JavaScript在執(zhí)行的過程中,如果使用了未定義的變量,則會報錯

2、在一個變量定義之前使用,不會報錯,只是其值是undefined。

3、在一個函數(shù)定義前使用它,并不會報錯,而是會正確執(zhí)行

第一個結(jié)論我們很容易理解,因為變量未被定義,所以在使用的時候肯定是找不到,因此必然會報錯。但是對于第二個和第三個結(jié)論,確實讓人費解的:變量和函數(shù)為什么能在其定義之前使用?這似乎表明JS代碼并不是按之前說的自上而下依次執(zhí)行的。

另外一點,就是同樣的方式,變量和函數(shù)的處理結(jié)果為什么不一樣?如上面的執(zhí)行結(jié)果,提前使用的showName函數(shù)能打印出來完整結(jié)果,但是提前使用的myName變量值卻是undefined,而不是我們定義時使用的“修謙”的這個值。要解釋這個,就不得不說到JavaScript中的一個很重要的概念:變量提升

1、什么是JavaScript的變量提升(Hoisting)

在說JavaScript的變量提升之前,我們得要先說一下JavaScript中的聲明和賦值操作,對于如下的這行代碼

1 var myName = '修謙'

實際上,這句代碼你可以把它分為兩部分來看,即聲明賦值

1 var myName //  變量聲明
2 myName = '修謙' // 變量賦值

以上的這個是JavaScript中變量的聲明和賦值,我們再來看一下JavaScript中的函數(shù)聲明和賦值操作是什么樣的,我們還是看以下這段代碼

1 function showName() {
2     console.log('我的名字叫修謙')
3 }
4 
5 var showName = function() {
6     console.log('我的名字叫修謙')
7 }

我們可以看出第一個函數(shù)showName是一個完整的函數(shù)聲明,它沒有涉及到賦值操作;第二個函數(shù)是先聲明變量showName,再把function(){console.log('我的名字叫修謙')}賦值給了showName。到這里你應(yīng)該知道了JavaScript中的變量聲明和賦值是怎么回事了。

說完了JavaScript中的變量聲明和賦值是怎么回事后,我們再來說JavaScript中的變量提升。

 在JavaScript中,所謂的變量提升:是指在 JavaScript 代碼執(zhí)行過程中,JavaScript 引擎把變量的聲明部分和函數(shù)的聲明部分提升到代碼開頭的一種“行為”。當(dāng)變量被提升后,會給變量設(shè)置默認值,而其所設(shè)置的默認值就是我們最為熟悉的undefined。從這個概念的字面意義上來看,“變量提升”意味著變量和函數(shù)的聲明會在物理層面移動到代碼的最前面

但其實這樣說也并不準(zhǔn)確。因為實際上,在JavaScript中,變量和函數(shù)的聲明在代碼里的位置是不會改變的。為什么呢?因為在JavaScript中,一段代碼的執(zhí)行是需要先經(jīng)過JavaScript引擎先編譯的,當(dāng)代碼編譯完后,才會進入到代碼的執(zhí)行階段(下圖所示)。說變量和函數(shù)的聲明在代碼里的位置是不會改變的原因,是因為代碼在編譯階段便已經(jīng)被JavaScript引擎放入到了內(nèi)存中(既然放到了內(nèi)存當(dāng)中,那么其位置當(dāng)然就已經(jīng)固定)。

那既然在編譯階段就在內(nèi)存中固定了位置,為什么又會出現(xiàn)提升呢?編譯階段和變量提升存在什么關(guān)系呢?這里我們就不得不說到另外一個概念:執(zhí)行上下文(Execution context)

2、執(zhí)行上下文(Execution context)

所謂執(zhí)行上下文,我們可以簡單的理解為就是 JavaScript 執(zhí)行一段代碼時的運行環(huán)境,比如當(dāng)我們在JavaScript文件中調(diào)用一個函數(shù),那么就會進入這個函數(shù)的執(zhí)行上下文,就會確定該函數(shù)在執(zhí)行期間用到的諸如 this、變量、對象以及函數(shù)等。并且在執(zhí)行上下文中還存在一個變量環(huán)境的對象(Viriable Environment),這是非常重要的。因為該對象中保存了變量提升的內(nèi)容,比如上面代碼中的變量myName和函數(shù)showName,都會保存在該對象中(我們先用下面的這段代碼模擬一下,后面在詳細講解)。

1 ViriableEnvironment(變量環(huán)境)
2     myName -> undefined
3     showName -> function: {console.log(myName)}

在JavaScript中,執(zhí)行上下文一般分為以下三種:

1、全局執(zhí)行上下文:當(dāng) JavaScript 執(zhí)行全局代碼的時候,會編譯全局代碼并創(chuàng)建全局執(zhí)行上下文,而且在整個頁面的生存周期內(nèi),全局執(zhí)行上下文只有一份。

2、函數(shù)執(zhí)行上下文:當(dāng)調(diào)用一個函數(shù)的時候,函數(shù)體內(nèi)的代碼會被編譯,并創(chuàng)建函數(shù)執(zhí)行上下文,在一般情況下,函數(shù)執(zhí)行結(jié)束之后,創(chuàng)建的函數(shù)執(zhí)行上下文會被銷毀。

3、eval :當(dāng)使用 eval 函數(shù)的時候,eval 的代碼也會被編譯,并創(chuàng)建執(zhí)行上下文。

但是我們現(xiàn)在常接觸或者說的一般都是指前面兩者。了解完執(zhí)行上下文的概念和分類后,我們再來了解一下另外的兩個知識點:函數(shù)執(zhí)行(調(diào)用)

3、函數(shù)執(zhí)行(調(diào)用)

函數(shù)調(diào)用概念很簡單,簡單一點來說就是運行一個函數(shù),具體使用方式是使用函數(shù)名稱跟著一對小括號。我們舉個例子來說一下

1 var myName = '修謙'
2 function showName() {
3     console.log('我的名字叫修謙')
4 }
5 
6 showName() // 執(zhí)行

這段代碼很簡單。首先我們創(chuàng)建了一個名叫myName的變量,接著又創(chuàng)建了一個showName的函數(shù)。完后緊接著在最后面調(diào)用執(zhí)行了該方法。下面我們就以這段簡單的代碼來說一下函數(shù)調(diào)用的過程。

當(dāng)執(zhí)行到函數(shù)showName()之前,JavaScript 引擎會為上面這段代碼創(chuàng)建全局執(zhí)行上下文,包含聲明的函數(shù)和變量,如下圖所示:

從圖中可以看出,上面那段代碼中全局變量和函數(shù)都保存在全局上下文的變量環(huán)境中。當(dāng)執(zhí)行上下文準(zhǔn)備好之后,JavaScript引擎便開始執(zhí)行全局代碼,當(dāng)執(zhí)行到showName函數(shù)時,JavaScript判斷出這是一個函數(shù)調(diào)用,于是便開始了以下操作:

1、首先,從全局執(zhí)行上下文中,取出showName函數(shù)代碼。
2、其次,對showName函數(shù)的這段代碼進行編譯,并創(chuàng)建該函數(shù)的執(zhí)行上下文和可執(zhí)行代碼。
3、最后,執(zhí)行代碼,輸出結(jié)果。

我們可以用一張相對完整的圖來描述 

當(dāng)執(zhí)行到showName函數(shù)的時候,我們就有了兩個執(zhí)行上下文了——全局執(zhí)行上下文和 showName 函數(shù)本身的執(zhí)行上下文(函數(shù)執(zhí)行上下文)。這也就是說在執(zhí)行JavaScript 時,會存在多個執(zhí)行上下文。那當(dāng)有多個上下文的時候,JavaScript引擎是如何管理的呢?這就是我們下面要說到的一種數(shù)據(jù)結(jié)構(gòu)——

4、棧(Stack)

棧(Stack)是一種線性數(shù)據(jù)結(jié)構(gòu),遵循后進先出(Last In, First Out, LIFO)的原則。這意味著最后進入棧的元素會最先被移除。如下圖所示,最先進入的是A,但是最先出的卻是E。而avaScript引擎正是利用棧的這種結(jié)構(gòu)來管理執(zhí)行上下文的。在執(zhí)行上下文創(chuàng)建好后,JavaScript引擎會將執(zhí)行上下文壓入棧中,然后進行執(zhí)行,而通常把這種用來管理執(zhí)行上下文的棧稱為執(zhí)行上下文棧,或者叫JavaScript調(diào)用棧

4、執(zhí)行上下文棧(JavaScript調(diào)用棧

下面我們就來具體的用代碼和圖來模擬JS執(zhí)行上下文棧是如何執(zhí)行代碼的,如下面一段代碼(以ES5來演示)

 1 var a = 2
 2 function add(b,c){
 3   return b+c
 4 }
 5 function addAll(b,c){
 6   var d = 2
 7   result = add(b,c)
 8   return a+result+d
 9 }
10 addAll(3,3)

第一步:創(chuàng)建全局上下,并將其壓入棧底(如圖所示)此時變量a、函數(shù)add 以及 addAll 都保存到了全局上下文的變量環(huán)境對象中。 

 當(dāng)全局執(zhí)行上下文壓入到調(diào)用棧后,緊接著,JavaScript引擎便開始執(zhí)行全局代碼了。首先會執(zhí)行a=2的賦值操作,賦值完后,此前a的值就從undefined變成了2。因為此時的add函數(shù)和addAll函數(shù)都還沒有執(zhí)行,因此狀態(tài)還是之前的。這一步完成后,我們再來看全局上下文的狀態(tài),如下圖所示:

第二步:執(zhí)行addAll函數(shù),此時JavaScript引擎會編譯該函數(shù),并為其創(chuàng)建一個執(zhí)行上下文,然后將其執(zhí)行上下文壓入棧中,如圖所示:

同樣的,當(dāng)addAll函數(shù)的執(zhí)行上下文創(chuàng)建好之后,就會進入了函數(shù)代碼的執(zhí)行階段了,因為函數(shù)中有一個變量d,因此還是先執(zhí)行賦值操作,即將d的值從之前的d=undefined設(shè)置成d=10 。然后接著往下執(zhí)行。

第三步:執(zhí)行add函數(shù),當(dāng)執(zhí)行到add函數(shù)調(diào)用語句時,JavaScript引擎同樣又會為其創(chuàng)建執(zhí)行上下文,并將其壓入調(diào)用棧,此時的調(diào)用棧的狀態(tài)如下圖所示:

然后add函數(shù)執(zhí)行,將返回結(jié)果賦值給變量result,此時的result的值便從之前的undefined變成了6。隨后該函數(shù)的執(zhí)行上下文便從從棧頂彈出。此時的調(diào)用棧如下圖所示:

緊接著addAll執(zhí)行最后一個相加操作后并返回,完成之后,addAll的執(zhí)行上下文也會從棧頂部彈出,此時調(diào)用棧中就只剩下全局上下文了。最終如下圖所示。 至此,整個JavaScript的執(zhí)行便完成了。

通過以上的分析,我們可以知道,正是由于JavaScript存在變量提升這種特性,從而導(dǎo)致了我們在日常的學(xué)習(xí)或者工作中,總是能看到很多與直覺不符或者說與我們思習(xí)慣不一樣的代碼,而這也是JavaScript的一個重要設(shè)計缺陷。為此, ECMAScript6引入塊級作用域的概念并配合 let、const 關(guān)鍵字,來避開了這種設(shè)計缺陷(這個我們接下來就會說)。但是在說之前,我們還要繼續(xù)說變量提升剩余的兩個問題:為什么JS中會出現(xiàn)變量提升?變量提升有什么缺點?

5、JS中變量提升的原因

我們都知道在ES6 之前,JavaScript是不支持塊級作用域的。因為當(dāng)初設(shè)計這門語言的時候,只是按照最簡單的方式來設(shè)計的。即只設(shè)計了全局作用域函數(shù)作用域以此來簡化JavaScript代碼的解析和執(zhí)行過程。可沒有想到的是 JavaScript后面會這么火,最后其沒有塊級作用域的缺陷便慢慢暴露了出來

既然問題已經(jīng)暴露出來了的話,那就解決問題。但是你不可能貿(mào)然的立馬增加塊級作用域吧!畢竟已經(jīng)用JavaScript這門語言開發(fā)了那么多應(yīng)用。于是就采取了一個不是特別激進的方法——把作用域內(nèi)部的變量統(tǒng)一提升。這也是彼時最快速,也是最簡單的方式。

當(dāng)然了任何事物都有兩面性。這一做法的一個很大的缺點就是直接導(dǎo)致了函數(shù)中的變量無論是在哪里聲明的,在編譯階段都會被提取到執(zhí)行上下文的變量環(huán)境中,所以這些變量在整個函數(shù)體內(nèi)部的任何地方都是能被訪問的,而這也就是我們通常說的JS 中的變量提升。

6、JS中變量提升的問題

1、變量在不知不覺中就被覆蓋

我們先來看下面的一段代碼,你認為會輸出什么結(jié)果?是修謙?是吳門山人

 1 var myName = "修謙"
 2 
 3 function showName(){
 4   console.log(myName);
 5   if(0){
 6    var myName = "吳門山人"
 7   }
 8   console.log(myName);
 9 }
10 
11 showName()

其實你把代碼執(zhí)行的話,會發(fā)現(xiàn)其輸出的結(jié)果兩者都不是。而是輸出了undefined。為什么會這樣呢?你可以參照前面舉的那個JS執(zhí)行的例子來自己試著畫一下過程圖。這里我們就直接貼最后的執(zhí)行棧圖。

當(dāng)showName函數(shù)的執(zhí)行上下文創(chuàng)建后,JavaScript引擎便開始執(zhí)行其內(nèi)部的代碼。首先執(zhí)行的是console.log(myName)。而執(zhí)行這段代碼需要使用變量myName,而從圖上我們可以看到,這里有兩個myName變量:一個是在全局執(zhí)行上下文中,其值是“修謙”;另外一個則是在showName函數(shù)的執(zhí)行上下文中,其值是undefined。這個時候JS到底要使用哪一個輸出呢?作為一個前端開發(fā)人員,我想絕大部分人都會說出正確的答案:“肯定是先使用showName函數(shù)執(zhí)行上下文里面的變量啦!”

的確是這樣,因為函數(shù)執(zhí)行過程中,JavaScript會優(yōu)先從當(dāng)前所在的執(zhí)行上下文中查找變量,但是因為變量提升的原因,當(dāng)前的執(zhí)行上下文中就包含了變量myName,而其值是undefined,所以獲取到的myName的值就是undefined。而不是如其它語言一樣,會輸出“修謙”

2. 本應(yīng)銷毀的變量沒有被銷毀

那既然在JavaScript中,變量提升會帶來上面說到的那些個問題?最后的解決方案又是什么呢?答案就是在2015年的時候發(fā)布了新的JS標(biāo)準(zhǔn)——ECMAScript6(簡稱ES6)。在 該標(biāo)準(zhǔn)中,正式引入了塊級作用域的概念。并且還引入了 let  const 關(guān)鍵字來聲明塊級作用域,至此,JavaScript也能像其他語言一樣擁有了塊級作用域。

7、ES6中的let和const

關(guān)于letconst。我們還是先來看如下的代碼

1 let myName = '修謙'
2 const myAag = 35
3 myName = '山人'
4 console.log(myName)
5 
6 myAag = 18
7 console.log(myAag)

這段代碼輸出的結(jié)果,我覺得只要是寫過JavaScript的人都應(yīng)該知道結(jié)果是啥。第一個輸出的是“山人”;而第二個則輸出一個錯誤。從這里我們可以看出,雖然兩者都是用來聲明塊級作用域的,但是兩者之間還是有區(qū)別的,使用 let 關(guān)鍵字聲明的變量是可以被改變的,而使用 const 聲明的變量其值是不可以被改變的。說到這里我們也順帶說一下面試中常被問到的一個問題:在JavaScript中,什么是暫時性死區(qū)?

還是先看代碼

1 function example() {
2   console.log(x); 
3   let x = 10;
4 }
5 
6 example();

當(dāng)我們把這段代碼復(fù)制到到瀏覽器控制臺的時候會報這樣一個錯誤: “ReferenceError: Cannot access 'x' before initialization”。這個錯誤翻譯過來是:引用錯誤:初始化之前無法訪問“x”(翻譯的可能不準(zhǔn),但是意思差不多)。從這個錯誤我們知道了在ES6中,當(dāng)我們用letconst 聲明的變量在聲明之前是處于一種“未初始化”狀態(tài),而這種狀態(tài)被稱為暫時性死區(qū)(官方的定義是:在 JavaScript 中,"暫時性死區(qū)"(Temporal Dead Zone, TDZ)是指在塊級作用域(如 let 和 const 聲明的變量所在的代碼塊)中,在變量聲明之前訪問該變量會導(dǎo)致引用錯誤(ReferenceError))。

 說完letconst,我們再來看以下的這兩行簡單的代碼

1 var myName = '修謙'
2 let myAag = 35

這兩行代碼其實并沒有什么特別的,我用其來就只是為了引出一個問題,即:JavaScript是怎么樣在支持變量提升特性的同時又支持塊級作用域的呢?因為我們在項目中,有時候你會發(fā)現(xiàn)有的人在代碼中即會用var關(guān)鍵字來聲明變量,同時又用letconst來聲明變量。雖然這種方式不推薦,但是總歸是不可避免的。前面我們已經(jīng)談到了變量提升特性。所以接下來我們重點談的就是JavaScript是如何支持塊級作用域的。

8、JavaScript 是如何支持塊級作用域的?

前面我們說到,在JavaScript引擎中是通過變量環(huán)境實現(xiàn)函數(shù)級作用域的,那么在 ES6 中,又是如何在其基礎(chǔ)之上,實現(xiàn)對塊級作用域的支持呢?我們還是先來看下面的一段代碼

 1 function showName(){
 2     var myName = '修謙'
 3     let myAag = 35
 4     {
 5       let myAag = 18
 6       var heName = '華仔'
 7       let heAge = 63
 8       console.log(myName)
 9       console.log(myAag)
10     }
11 }   
12 showName()

當(dāng)執(zhí)行上面這段代碼的時候,JavaScript引擎會先對其進行編譯并創(chuàng)建執(zhí)行上下文,然后再按照順序執(zhí)行代碼,之前我們的例子是沒有使用ES6中的關(guān)鍵字let。但是現(xiàn)在引入了 let 關(guān)鍵字,它會創(chuàng)建塊級作用域,那么它是如何影響執(zhí)行上下文的呢?這里我們就不得不提到一個名詞——詞法環(huán)境。你應(yīng)該還記得之前的例子中,右邊一直有一塊空著的,名叫詞法環(huán)境的塊。而JavaScript之所以支持塊級作用域,就是與它有關(guān)。

下面我們還是按照之前的方式來梳理一下這段代碼的執(zhí)行。

第一步:編譯并創(chuàng)建全局執(zhí)行上下文 

第二步:執(zhí)行showName函數(shù),為其創(chuàng)建函數(shù)執(zhí)行上下文

showName函數(shù)執(zhí)行這一步。我么可以從調(diào)用棧中看出:

1、函數(shù)內(nèi)部通過 var 聲明的變量,在編譯階段全都被存放到變量環(huán)境里面了(這個和之前的一樣)
2、通過 let 聲明的變量,在編譯階段則會被存放到詞法環(huán)境(Lexical Environment)中
3、在函數(shù)的作用域塊內(nèi)部,通過 let 聲明的變量并沒有被存放到詞法環(huán)境中

第三步:繼續(xù)往下執(zhí)行代碼。當(dāng)執(zhí)行到代碼塊里面時,變量環(huán)境中myName的值已經(jīng)被設(shè)置成了"修謙",而詞法環(huán)境中myAag的值則被設(shè)置成了35,此時的函數(shù)的執(zhí)行上下文如下圖所示:

從第三步的圖中我們可以看出,當(dāng)進入函數(shù)內(nèi)部的作用域塊時,作用域塊中通過 let 聲明的變量(myAagheAag),會被存放在詞法環(huán)境的一個單獨的區(qū)域中,且不影響作用域塊外面的變量(之前的myAag)。因此它們都是獨立的存在。另外我們從中也可以看出,其實在詞法環(huán)境內(nèi)部,也是維護了一個小型棧結(jié)構(gòu),棧底是函數(shù)最外層的變量(即內(nèi)部作用域塊外邊的變量,這里就是myAag),當(dāng)進入某一個作用域塊后,就會把該作用域塊內(nèi)部的變量壓到棧頂(myAagheAag);當(dāng)作用域執(zhí)行完成之后,該作用域的信息就會從棧頂彈出,而這就是詞法環(huán)境的結(jié)構(gòu)(前提就是必須用let或者const關(guān)鍵字定義)。

第四步:繼續(xù)往下執(zhí)行代碼。將作用塊中的myAagheAag分別賦值為16,63,同時也將環(huán)境變量中的heName的值賦值為“華仔”。如圖所示

第五步:繼續(xù)往下執(zhí)行代碼。當(dāng)執(zhí)行到作用域塊中的console.log(myName)這行代碼時,此時就需要在詞法環(huán)境變量環(huán)境中查找變量myName的值了,而具體查找方式是:沿著當(dāng)前詞法環(huán)境的棧頂向下查詢,如果在詞法環(huán)境中的某個塊中查找到了,就直接返回給JavaScript引擎,如果沒有查找到,那么繼續(xù)在變量環(huán)境中查找(同樣的,作用域塊中的console.log(myAag)也是這樣的規(guī)則)。此時如下圖所示:因為在詞法環(huán)境中沒有找到myName的這個變量,因此就會去變量環(huán)境中去找,最終在變量環(huán)境中找到了myName(黃色箭頭所指),因此輸出“修謙”同樣console.log(myAag)因為在詞法環(huán)境中找到了myAag(深藍色箭頭所指),因此輸出18。而將上面的代碼在瀏覽器里執(zhí)行,也是這樣的結(jié)果

 

當(dāng)函數(shù)內(nèi)部作用域塊執(zhí)行結(jié)束之后,其內(nèi)部定義的變量就會從詞法環(huán)境的棧頂彈出,最終的執(zhí)行上下文如下圖所示:

通過上面的分析,我們基本已經(jīng)理解了詞法環(huán)境的結(jié)構(gòu)和工作機制:ES6中的塊級作用域就是通過詞法環(huán)境的棧結(jié)構(gòu)來實現(xiàn)的,而之前的變量提升是通過變量環(huán)境來實現(xiàn),通過這兩者的結(jié)合,JS 引擎也就同時支持了變量提升和塊級作用域了。至此,我想關(guān)于變量提升,你應(yīng)該有一個比較深刻的印象了。當(dāng)然了,上面寫的可能并不完全正確。也歡迎大家指正批評。

?轉(zhuǎn)自https://www.cnblogs.com/xiuqian/p/18595873



該文章在 2024/12/17 15:53:47 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
亚洲成AV人片在线观看不卡 | 午夜久久福利小视频 | 亚洲国产日韩一级视频网站 | 午夜高清国产拍精品福利 | 先锋资源视频在线资源 | 亚洲乱码国产乱码精品精在线观看 |