欧美成人精品手机在线观看_69视频国产_动漫精品第一页_日韩中文字幕网 - 日本欧美一区二区

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

[點(diǎn)晴永久免費(fèi)OA]JS 愛好者的十大反向教學(xué)(譯)

freeflydom
2023年11月28日 15:26 本文熱度 943

大家好,這里是大家的林語冰。

免責(zé)聲明

本文屬于是語冰的直男翻譯了屬于是,僅供粉絲參考,英文原味版請臨幸 The 10 Most Common Javascript Issues Developers Face。

今時(shí)今日,JS(Javascript)幾乎是所有現(xiàn)代 Web App 的核心。這就是為什么 JS 出問題,以及找到導(dǎo)致這些問題的錯(cuò)誤,是 Web 開發(fā)者的最前線。

用于 SPA(單頁應(yīng)用程序)開發(fā)、圖形和動畫以及服務(wù)器端 JS 平臺的給力的 JS 庫和框架不足為奇。JS 在 Web App 開發(fā)領(lǐng)域早已無處不在,因此是一項(xiàng)越來越需要加點(diǎn)的技能樹。

乍一看,JS 可能很簡單。事實(shí)上,對于任何有經(jīng)驗(yàn)的軟件開發(fā)者而言,哪怕它們是 JS 初學(xué)者,將基本的 JS 功能構(gòu)建到網(wǎng)頁中也是舉手之勞。

雖然但是,這種語言比大家起初認(rèn)為的要更微妙、給力和復(fù)雜。事實(shí)上,一大坨 JS 的微妙之處可能導(dǎo)致一大坨常見問題,無法正常工作 —— 我們此處會討論其中的 10 個(gè)問題。在成為 JS 大神的過程中,了解并避免這些問題十分重要。

問題 1:this 引用失真

JS 開發(fā)者對 JS 的 this 關(guān)鍵字不乏困惑。

多年來,隨著 JS 編碼技術(shù)和設(shè)計(jì)模式越來越復(fù)雜,回調(diào)和閉包中自引用作用域的延伸也同比增加,此乃導(dǎo)致 JS “this 混淆”問題的“萬惡之源”。

請瞄一眼下述代碼片段:

const Game = function () {

  this.clearLocalStorage = function () {

    console.log('Clearing local storage...')

  }

  this.clearBoard = function () {

    console.log('Clearing board...')

  }

}


Game.prototype.restart = function () {

  this.clearLocalStorage()

  this.timer = setTimeout(function () {

    this.clearBoard() // this 是什么鬼物?

  }, 0)

}


const myGame = new Game()

myGame.restart()


執(zhí)行上述代碼會導(dǎo)致以下錯(cuò)誤:

未捕獲的類型錯(cuò)誤: this.clearBoard 不是函數(shù)


為什么呢?這與上下文有關(guān)。出現(xiàn)該錯(cuò)誤的原因是,當(dāng)您執(zhí)行 setTimeout() 時(shí),您實(shí)際是在執(zhí)行 window.setTimeout()。因此,傳遞給 setTimeout() 的匿名函數(shù)定義在 window 對象的上下文中,該對象沒有 clearBoard() 方法。

一個(gè)傳統(tǒng)的、兼容舊瀏覽器的技術(shù)方案是簡單地將您的 this 引用保存在一個(gè)變量中,然后可以由閉包繼承,舉個(gè)栗子:

Game.prototype.restart = function () {

  this.clearLocalStorage()

  const self = this // 當(dāng) this 還是 this 的時(shí)候,保存 this 引用!

  this.timer = setTimeout(function () {

    self.clearBoard() // OK,我們可以知道 self 是什么了!

  }, 0)

}


或者,在較新的瀏覽器中,您可以使用 bind() 方法傳入正確的引用:

Game.prototype.restart = function () {

  this.clearLocalStorage()

  this.timer = setTimeout(this.reset.bind(this), 0) // 綁定 this

}


Game.prototype.reset = function () {

  this.clearBoard() // OK,回退到正確 this 的上下文!

}


問題 2:認(rèn)為存在塊級作用域

JS 開發(fā)者之間混淆的“萬惡之源”之一(因此也是 bug 的常見來源)是,假設(shè) JS 為每個(gè)代碼塊創(chuàng)建新的作用域。盡管這在許多其他語言中是正確的,但在 JS 中卻并非如此。舉個(gè)栗子,請瞄一眼下述代碼:

for (var i = 0; i < 10; i++) {

  /* ... */

}

console.log(i) // 輸出是什么鬼物?


如果您猜到調(diào)用 console.log() 會輸出 undefined 或報(bào)錯(cuò),那么恭喜您猜錯(cuò)了。信不信由你,它會輸出 10。為什么呢?

在大多數(shù)其他語言中,上述代碼會導(dǎo)致錯(cuò)誤,因?yàn)樽兞?i 的“生命”(即作用域)將被限制在 for 區(qū)塊中。雖然但是,在 JS 中,情況并非如此,即使在循環(huán)完成后,變量 i 仍保留在范圍內(nèi),在退出 for 循環(huán)后保留其最終值。(此行為被稱為變量提升。)

JS 對塊級作用域的支持可通過 let 關(guān)鍵字獲得。多年來,let 關(guān)鍵字一直受到瀏覽器和后端 JS 引擎(比如 Node.js)的廣泛支持。如果這對您來說是新知識,那么值得花時(shí)間閱讀作用域、原型等。

問題3:創(chuàng)建內(nèi)存泄漏

如果您沒有刻意編碼來避免內(nèi)存泄漏,那么內(nèi)存泄漏幾乎不可避免。它們有一大坨觸發(fā)方式,因此我們只強(qiáng)調(diào)其中兩種更常見的情況。

示例 1:失效對象的虛空引用

注意:此示例僅適用于舊版 JS 引擎,新型 JS 引擎具有足夠機(jī)智的垃圾回收器(GC)來處理這種情況。

請瞄一眼下述代碼:

var theThing = null

var replaceThing = function () {

  var priorThing = theThing // 保留之前的東東

  var unused = function () {

    // unused 是唯一引用 priorThing 的地方,

    // 但 unused 從未執(zhí)行

    if (priorThing) {

      console.log('hi')

    }

  }

  theThing = {

    longStr: new Array(1000000).join('*'), // 創(chuàng)建一個(gè) 1MB 的對象

    someMethod: function () {

      console.log(someMessage)

    }

  }

}

setInterval(replaceThing, 1000) // 每秒執(zhí)行一次 replaceThing


如果您運(yùn)行上述代碼并監(jiān)視內(nèi)存使用情況,就會發(fā)現(xiàn)嚴(yán)重的內(nèi)存泄漏 —— 每秒有一整兆字節(jié)!即使是手動垃圾收集器也無濟(jì)于事。所以看起來每次調(diào)用 replaceThing 時(shí)我們都在泄漏 longSte。但是為什么呢?

如果您沒有刻意編碼來避免內(nèi)存泄漏,那么內(nèi)存泄漏幾乎不可避免。

讓我們更詳細(xì)地檢查一下:

每個(gè) theThing 對象都包含自己的 1MB longStr 對象。每一秒,當(dāng)我們調(diào)用 replaceThing 時(shí),它都會保留 priorThing 中之前的 theThing 對象的引用。但我們?nèi)匀徊徽J(rèn)為這是一個(gè)問題,因?yàn)槊看蜗惹耙玫?priorThing 都會被取消引用(當(dāng) priorThing 通過 priorThing = theThing; 重置時(shí))。此外,它僅在 replaceThing 的主體中和 unused 函數(shù)中被引用,這實(shí)際上從未使用過。

因此,我們再次想知道為什么這里存在內(nèi)存泄漏。

要了解發(fā)生了什么事,我們需要更好地理解 JS 的內(nèi)部工作原理。閉包通常由鏈接到表示其詞法作用域的字典風(fēng)格對象(dictionary-style)的每個(gè)函數(shù)對象實(shí)現(xiàn)。如果 replaceThing 內(nèi)部定義的兩個(gè)函數(shù)實(shí)際使用了 priorThing,那么它們都得到相同的對象是很重要的,即使 priorThing 逐次賦值,兩個(gè)函數(shù)也共享相同的詞法環(huán)境。但是,一旦任何閉包使用了變量,它就會進(jìn)入該作用域中所有閉包共享的詞法環(huán)境中。而這個(gè)小小的細(xì)微差別就是導(dǎo)致這種粗糙的內(nèi)存泄漏的原因。

示例 2:循環(huán)引用

請瞄一眼下述代碼片段:

function addClickHandler(element) {

  element.click = function onClick(e) {

    alert('Clicked the ' + element.nodeName)

  }

}


此處,onClick 有一個(gè)閉包,它保留了 element 的引用(通過 element.nodeName)。通過同時(shí)將 onClick 賦值給 element.click,就創(chuàng)建了循環(huán)引用,即 element -> onClick -> element -> onClick -> element ......

有趣的是,即使 element 從 DOM 中刪除,上述循環(huán)自引用也會阻止 elementonClick 被回收,從而造成內(nèi)存泄漏。

避免內(nèi)存泄漏:要點(diǎn)

JS 的內(nèi)存管理(尤其是它的垃圾回收)很大程度上基于對象可達(dá)性(reachability)的概念。

假定以下對象是可達(dá)的,稱為“根”:

  • 從當(dāng)前調(diào)用堆棧中的任意位置引用的對象(即,當(dāng)前正在執(zhí)行的函數(shù)中的所有局部變量和參數(shù),以及閉包作用域中的所有變量)

  • 所有全局變量

只要對象可以通過引用或引用鏈從任何根訪問,那么它們至少會保留在內(nèi)存中。

瀏覽器中有一個(gè)垃圾回收器,用于清理不可達(dá)對象占用的內(nèi)存;換而言之,當(dāng)且僅當(dāng) GC 認(rèn)為對象不可達(dá)時(shí),才會從內(nèi)存中刪除對象。不幸的是,很容易得到已失效的“僵尸”對象,這些對象不再使用,但 GC 仍然認(rèn)為它們可達(dá)。

問題 4:混淆相等性

JS 的便捷性之一是,它會自動將布爾上下文中引用的任何值強(qiáng)制轉(zhuǎn)換為布爾值。但在某些情況下,這可能既香又臭。

舉個(gè)栗子,對于一大坨 JS 開發(fā)者而言,下列表達(dá)式很頭大:

// 求值結(jié)果均為 true!

console.log(false == '0');

console.log(null == undefined);

console.log(" \t\r\n" == 0);

console.log('' == 0);


// 這些也是 true!

if ({}) // ...

if ([]) // ...


關(guān)于最后兩個(gè),盡管是空的(這可能會讓您相信它們求值為 false),但 {}[] 實(shí)際上都是對象,并且 JS 中任何對象都將被強(qiáng)制轉(zhuǎn)換為 true,這與 ECMA-262 規(guī)范一致。

正如這些例子所表明的,強(qiáng)制類型轉(zhuǎn)換的規(guī)則有時(shí)可以像泥巴一樣清晰。因此,除非明確需要強(qiáng)制類型轉(zhuǎn)換,否則通常最好使用 ===!==(而不是 ==!=)以避免強(qiáng)制類型轉(zhuǎn)換的任何意外副作用。(==!= 比較兩個(gè)東東時(shí)會自動執(zhí)行類型轉(zhuǎn)換,而 ===!== 在不進(jìn)行類型轉(zhuǎn)換的情況下執(zhí)行同款比較。)

由于我們談?wù)摰氖菑?qiáng)制類型轉(zhuǎn)換和比較,因此值得一提的是,NaN 與任何事物(甚至 NaN 自己?。┻M(jìn)行比較始終會返回 false。因此您不能使用相等運(yùn)算符( =====,!=!==)來確定值是否為 NaN。請改用內(nèi)置的全局 isNaN() 函數(shù):

console.log(NaN == NaN) // False

console.log(NaN === NaN) // False

console.log(isNaN(NaN)) // True


問題 5:低效的 DOM 操作

JS 使得操作 DOM 相對容易(即添加、修改和刪除元素),但對提高操作效率沒有任何作用。

一個(gè)常見的示例是一次添加一個(gè) DOM 元素的代碼。添加 DOM 元素是一項(xiàng)代價(jià)昂貴的操作,連續(xù)添加多個(gè) DOM 元素的代碼效率低下,并且可能無法正常工作。

當(dāng)需要添加多個(gè) DOM 元素時(shí),一個(gè)有效的替代方案是改用文檔片段(document fragments),這能提高效率和性能。

舉個(gè)栗子:

const div = document.getElementById('my_div')

const fragment = document.createDocumentFragment()

const elems = document.queryselectorAll('a')


for (let e = 0; e < elems.length; e++) {

  fragment.appendChild(elems[e])

}

div.appendChild(fragment.cloneNode(true))


除了這種方法固有的提高效率之外,創(chuàng)建附加的 DOM 元素代價(jià)昂貴,而在分離時(shí)創(chuàng)建和修改它們,然后附加它們會產(chǎn)生更好的性能。

問題 6:在 for 循環(huán)中錯(cuò)誤使用函數(shù)定義

請瞄一眼下述代碼:

var elements = document.getElementsByTagName('input')

var n = elements.length // 我們假設(shè)本例有 10 個(gè)元素

for (var i = 0; i < n; i++) {

  elements[i].onclick = function () {

    console.log('This is element #' + i)

  }

}


根據(jù)上述代碼,如果有 10 個(gè)輸入元素,單擊其中任何一個(gè)都會顯示“This is element #10”!這是因?yàn)?,在為任何元素調(diào)用 onclick 時(shí),上述 for 循環(huán)將完成,并且 i 的值已經(jīng)是 10(對于所有元素)。

以下是我們?nèi)绾渭m正此問題,實(shí)現(xiàn)所需的行為:

var elements = document.getElementsByTagName('input')

var n = elements.length // 我們假設(shè)本例有 10 個(gè)元素

var makeHandler = function (num) {

  // 外部函數(shù)

  return function () {

    // 內(nèi)部函數(shù)

    console.log('This is element #' + num)

  }

}

for (var i = 0; i < n; i++) {

  elements[i].onclick = makeHandler(i + 1)

}


在這個(gè)修訂版代碼中,每次我們通過循環(huán)時(shí),makeHandler 都會立即執(zhí)行,每次都會接收當(dāng)時(shí) i + 1 的值并將其綁定到作用域的 num 變量。外部函數(shù)返回內(nèi)部函數(shù)(也使用此作用域的 num 變量),元素的 onclick 會設(shè)置為該內(nèi)部函數(shù)。這確保每個(gè) onclick 接收和使用正確的 i 值(通過作用域的 num 變量)。

問題 7:誤用原型式繼承

令人驚訝的是,一大坨 JS 愛好者無法完全理解和充分利用原型式繼承的特性。

下面是一個(gè)簡單的示例:

BaseObject = function (name) {

  if (typeof name !== 'undefined') {

    this.name = name

  } else {

    this.name = 'default'

  }

}


這似乎一目了然。如果您提供一個(gè)名稱,請使用該名稱,否則將名稱設(shè)置為“default”。舉個(gè)栗子:

var firstObj = new BaseObject()

var secondObj = new BaseObject('unique')


console.log(firstObj.name) // -> 結(jié)果是 'default'

console.log(secondObj.name) // -> 結(jié)果是 'unique'


但是,如果我們這樣做呢:

delete secondObj.name


然后我們會得到:

console.log(secondObj.name) // -> 結(jié)果是 'undefined'


騷然但是,將其恢復(fù)為“default”不是更好嗎?如果我們修改原始代碼以利用原型式繼承,這很容易實(shí)現(xiàn),如下所示:

BaseObject = function (name) {

  if (typeof name !== 'undefined') {

    this.name = name

  }

}


BaseObject.prototype.name = 'default'


在此版本中,BaseObject 從其 prototype 對象繼承該 name 屬性,其中該屬性(默認(rèn))設(shè)置為 'default'。因此,如果調(diào)用構(gòu)造函數(shù)時(shí)沒有名稱,那么名稱將默認(rèn)為 default。同樣,如果從 BaseObject 的實(shí)例刪除該 name 屬性,那么會搜索原型鏈,并從 prototype 對象中檢索值仍為 'default'name 屬性。所以現(xiàn)在我們得到:

var thirdObj = new BaseObject('unique')

console.log(thirdObj.name) // -> 結(jié)果是 'unique'


delete thirdObj.name

console.log(thirdObj.name) // -> 結(jié)果是 'default'


問題 8:創(chuàng)建對實(shí)例方法的錯(cuò)誤引用

讓我們定義一個(gè)簡單對象,并創(chuàng)建它的實(shí)例,如下所示:

var MyObjectFactory = function () {}


MyObjectFactory.prototype.whoAmI = function () {

  console.log(this)

}


var obj = new MyObjectFactory()


現(xiàn)在,為了方便起見,讓我們創(chuàng)建一個(gè) whoAmI 方法的引用,大概這樣我們就可以通過 whoAmI() 訪問它,而不是更長的 obj.whoAmI()

var whoAmI = obj.whoAmI


為了確保我們存儲了函數(shù)的引用,讓我們打印出新 whoAmI 變量的值:

console.log(whoAmI)


輸出:

function () {     console.log(this); }


目前它看起來不錯(cuò)。

但是瞄一眼我們調(diào)用 obj.whoAmI() 與便利引用 whoAmI() 時(shí)的區(qū)別:

obj.whoAmI() // 輸出 "MyObjectFactory {...}" (預(yù)期)

whoAmI() // 輸出 "window" (啊這!)


哪里出了問題?我們的 whoAmI() 調(diào)用位于全局命名空間中,因此 this 設(shè)置為 window(或在嚴(yán)格模式下設(shè)置為 undefined),而不是 MyObjectFactoryobj 實(shí)例!換而言之,該 this 值通常取決于調(diào)用上下文。

箭頭函數(shù)((params) => {} 而不是 function(params) {})提供了靜態(tài) this,與常規(guī)函數(shù)基于調(diào)用上下文的 this 不同。這為我們提供了一個(gè)技術(shù)方案:

var MyFactoryWithStaticThis = function () {

  this.whoAmI = () => {

    // 請注意此處的箭頭符號

    console.log(this)

  }

}


var objWithStaticThis = new MyFactoryWithStaticThis()

var whoAmIWithStaticThis = objWithStaticThis.whoAmI


objWithStaticThis.whoAmI() // 輸出 "MyFactoryWithStaticThis" (同往常一樣)

whoAmIWithStaticThis() // 輸出 "MyFactoryWithStaticThis" (箭頭符號的福利)


您可能已經(jīng)注意到,即使我們得到了匹配的輸出,this 也是對工廠的引用,而不是對實(shí)例的引用。與其試圖進(jìn)一步解決此問題,不如考慮根本不依賴 this(甚至不依賴 new)的 JS 方法。

問題 9:提供一個(gè)字符串作為 setTimeout or setInterval 的首參

首先,讓我們在這里明確一點(diǎn):提供字符串作為首個(gè)參數(shù)給 setTimeout 或者 setInterval 本身并不是一個(gè)錯(cuò)誤。這是完全合法的 JS 代碼。這里的問題更多的是性能和效率。經(jīng)常被忽視的是,如果將字符串作為首個(gè)參數(shù)傳遞給 setTimeoutsetInterval,它將被傳遞給函數(shù)構(gòu)造函數(shù)以轉(zhuǎn)換為新函數(shù)。這個(gè)過程可能緩慢且效率低下,而且通常非必要。

將字符串作為首個(gè)參數(shù)傳遞給這些方法的替代方法是傳入函數(shù)。讓我們舉個(gè)栗子。

因此,這里將是 setIntervalsetTimeout 的經(jīng)典用法,將字符串作為首個(gè)參數(shù)傳遞:

setInterval('logTime()', 1000) 
setTimeout("logMessage('" + msgValue + "')", 1000)


更好的選擇是傳入一個(gè)函數(shù)作為初始參數(shù),舉個(gè)栗子:

setInterval(logTime, 1000) // 將 logTime 函數(shù)傳給 setInterval


setTimeout(function () {

  // 將匿名函數(shù)傳給 setTimeout

  logMessage(msgValue) // (msgValue 在此作用域中仍可訪問)

}, 1000)


問題 10:禁用“嚴(yán)格模式”

“嚴(yán)格模式”(即在 JS 源文件的開頭包含 'use strict';)是一種在運(yùn)行時(shí)自愿對 JS 代碼強(qiáng)制執(zhí)行更嚴(yán)格的解析和錯(cuò)誤處理的方法,也是一種使代碼更安全的方法。

誠然,禁用嚴(yán)格模式并不是真正的“錯(cuò)誤”,但它的使用越來越受到鼓勵(lì),省略它越來越被認(rèn)為是不好的形式。

以下是嚴(yán)格模式的若干主要福利:

  • 更易于調(diào)試。本來會被忽略或靜默失敗的代碼錯(cuò)誤現(xiàn)在將生成錯(cuò)誤或拋出異常,更快地提醒您代碼庫中的 JS 問題,并更快地將您定位到其源代碼。

  • 防止意外全局變量。如果沒有嚴(yán)格模式,將值賦值給給未聲明的變量會自動創(chuàng)建同名全局變量。這是最常見的 JS 錯(cuò)誤之一。在嚴(yán)格模式下,嘗試這樣做會引發(fā)錯(cuò)誤。

  • 消除 this 強(qiáng)制類型轉(zhuǎn)換。如果沒有嚴(yán)格模式,對 nullundefined 值的 this 引用會自動強(qiáng)制轉(zhuǎn)換到 globalThis 變量。這可能會導(dǎo)致一大坨令人沮喪的 bug。在嚴(yán)格模式下,nullundefined 值的 this 引用會拋出錯(cuò)誤。

  • 禁止重復(fù)的屬性名或參數(shù)值。嚴(yán)格模式在檢測到對象中的重名屬性(比如 var object = {foo: "bar", foo: "baz"};)或函數(shù)的重名參數(shù)(比如 function foo(val1, val2, val1){})時(shí)會拋出錯(cuò)誤,從而捕獲代碼中幾乎必然出錯(cuò)的 bug,否則您可能會浪費(fèi)大量時(shí)間進(jìn)行跟蹤。

  • 更安全的 eval()。嚴(yán)格模式和非嚴(yán)格模式下 eval() 的行為存在某些差異。最重要的是,在嚴(yán)格模式下,eval() 語句中聲明的變量和函數(shù)不會在其包裹的作用域中創(chuàng)建。(它們在非嚴(yán)格模式下是在其包裹的作用域中創(chuàng)建的,這也可能是 JS 問題的常見來源。)

  • delete 無效使用時(shí)拋出錯(cuò)誤。delete 運(yùn)算符(用于刪除對象屬性)不能用于對象的不可配置屬性。當(dāng)嘗試刪除不可配置屬性時(shí),非嚴(yán)格代碼將靜默失敗,而在這種情況下,嚴(yán)格模式將拋出錯(cuò)誤。

使用更智能的方法緩解 JS 問題

與任何技術(shù)一樣,您越能理解 JS 奏效和失效的原因和方式,您的代碼就會越可靠,您就越能有效地利用語言的真正力量。

相反,缺乏 JS 范式和概念的正確理解是許多 JS 問題所在。徹底熟悉語言的細(xì)微差別和微妙之處是提高熟練度和生產(chǎn)力的最有效策略。


作者:人貓神話
鏈接:https://juejin.cn/post/7306040473542508556
來源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。



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