ES6之Proxy代理

什么是Proxy代理

ES6 讓開發(fā)者能進一步接近 JS 引擎的能力,這些能力原先只存在于內(nèi)置對象上。語言通過代
理( proxy )暴露了在對象上的內(nèi)部工作,代理是一種封裝,能夠攔截并改變 JS 引擎的底
層操作。

人話是:把代理看做是設(shè)計模式代理模式中的一種,有一個代理對象來代理本體,而ES6的Proxy牛逼的一點是可以把本體沒法改變的內(nèi)部屬性改了

代理與反射是什么?

通過調(diào)用 new Proxy() ,你可以創(chuàng)建一個代理用來替代另一個對象(被稱為目標(biāo)),這個代理對目標(biāo)對象進行了虛擬,因此該代理與該目標(biāo)對象表面上可以被當(dāng)作同一個對象來對待。

代理允許你攔截在目標(biāo)對象上的底層操作,而這原本是JS引擎的內(nèi)部能力。攔截行為使用了一個能夠響應(yīng)特定操作的函數(shù)(被稱為陷阱)。

被 Reflect 對象所代表的反射接口,是給底層操作提供默認行為的方法的集合,這些操作是能夠被代理重寫的。每個代理陷阱都有一個對應(yīng)的反射方法,每個方法都與對應(yīng)的陷阱函數(shù)同名,并且接收的參數(shù)也與之一致。下表總結(jié)了這些行為:

代理陷阱 被重寫的行為 默認行為
get 讀取一個屬性的值 Reflect.get()
set 寫入一個屬性 Reflect.set()
has in 運算符 Reflect.has()
deleteProperty delete 運算符 Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensiable() Reflect.setPrototypeOf()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty()
ownKeys Object.keys 、Object.getOwnPropertyNames() 與Object.getOwnPropertySymbols() Reflect.ownKeys()
apply 調(diào)用一個函數(shù) Reflect.apply()
construct 使用new調(diào)用一個函數(shù) Reflect.construct()

每個陷阱函數(shù)都可以重寫 JS 對象的一個特定內(nèi)置行為,允許你攔截并修改它。如果你仍然需
要使用原先的內(nèi)置行為,則可使用對應(yīng)的反射接口方法。一旦創(chuàng)建了代理,你就能清晰了解
代理與反射接口之間的關(guān)系,因此我們最好通過一些例子來進行深入研究。

創(chuàng)建一個簡單的代理

當(dāng)你使用 Proxy 構(gòu)造器來創(chuàng)建一個代理時,需要傳遞兩個參數(shù):目標(biāo)對象以及一個處理器(handler),后者是定義了一個或多個陷阱函數(shù)的對象。如果未提供陷阱函數(shù),代理會對所有操作采取默認行為。為了創(chuàng)建一個僅進行傳遞的代理,你需要使用不包含任何陷阱函數(shù)的處理器:

let target={}
let proxy=new Proxy(target,{})

proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"

使用 set 陷阱函數(shù)驗證屬性值

假設(shè)你想要創(chuàng)建一個對象,并要求其屬性值只能是數(shù)值,這就意味著該對象的每個新增屬性都要被驗證,并且在屬性值不為數(shù)值類型時應(yīng)當(dāng)拋出錯誤。為此你需要定義 set 陷阱函數(shù)來重寫設(shè)置屬性值時的默認行為,該陷阱函數(shù)能接受四個參數(shù):

  • trapTarget :將接收屬性的對象(即代理的目標(biāo)對象);
  • key :需要寫入的屬性的鍵(字符串類型或符號類型);
  • value :將被寫入屬性的值;
  • receiver :操作發(fā)生的對象(通常是代理對象)。

Reflect.set() 是 set 陷阱函數(shù)對應(yīng)的反射方法,同時也是set操作的默認行為。Reflect.set()方法與set陷阱函數(shù)一樣,能接受這四個參數(shù),讓該方法能在陷阱函數(shù)內(nèi)部被方便使用。該陷阱函數(shù)需要在屬性被設(shè)置完成的情況下返回 true ,否則就要返回 false,而 Reflect.set() 也會基于操作是否成功而返回相應(yīng)的結(jié)果。

你需要使用 set 陷阱函數(shù)來攔截傳入的 value 值,以便對屬性值進行驗證。這里有個例子:

let target = {
    name: "target"
};
let proxy = new Proxy(target, {
    set(trapTarget, key, value, receiver) {
    // 忽略已有屬性,避免影響它們
    if (!trapTarget.hasOwnProperty(key)) {
        if (isNaN(value)) {
            throw new TypeError("Property must be a number.");
        }
    }
    // 添加屬性
    return Reflect.set(trapTarget, key, value, receiver);
    }
});
// 添加一個新屬性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 你可以為 name 賦一個非數(shù)值類型的值,因為該屬性已經(jīng)存在
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 拋出錯誤
proxy.anotherName = "proxy";

使用 get 陷阱函數(shù)進行對象外形驗證

該陷阱函數(shù)會在讀取屬性時被調(diào)用,即使該屬性在對象中并不存在,它能接受三個參數(shù):

  • trapTarget :將會被讀取屬性的對象(即代理的目標(biāo)對象);
  • key :需要讀取的屬性的鍵(字符串類型或符號類型);
  • receiver :操作發(fā)生的對象(通常是代理對象)。

你可以使用 get 陷阱函數(shù)與 Reflect.get() 方法在目標(biāo)屬性不存在時拋出錯誤,就像這樣:

let proxy = new Proxy({}, {
    get(trapTarget, key, receiver) {
        if (!(key in receiver)) {
            throw new TypeError("Property " + key + " doesn't exist.");
        }
        return Reflect.get(trapTarget, key, receiver);
    }
});
// 添加屬性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 讀取不存在屬性會拋出錯誤
console.log(proxy.nme); // 拋出錯誤

使用 has 陷阱函數(shù)隱藏屬性

has 陷阱函數(shù)會在使用 in 運算符的情況下被調(diào)用,并且會被傳入兩個參數(shù):

  • trapTarget :需要讀取屬性的對象(即代理的目標(biāo)對象);
  • key :需要檢查的屬性的鍵(字符串類型或符號類型)。

Reflect.has() 方法接受與之相同的參數(shù),并向 in 運算符返回默認響應(yīng)結(jié)果。使用 has陷阱函數(shù)以及 Reflect.has() 方法,允許你修改部分屬性在接受 in 檢測時的行為,但保留其他屬性的默認行為。例如,假設(shè)你只想要隱藏 value 屬性,你可以這么做:

let target = {
    name: "target",
    value: 42
};
let proxy = new Proxy(target, {
    has(trapTarget, key) {
        if (key === "value") {
            return false;
        } else {
            return Reflect.has(trapTarget, key);
        }
    }
});
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true

這里的 proxy 對象使用了 has 陷阱函數(shù),用于檢查 key 值是否為 "value" 。如果是,則返回 false ;否則通過調(diào)用 Reflect.has() 方法來返回默認的結(jié)果。這樣,雖然 value 屬性確實存在于目標(biāo)對象中,但 in 運算符卻會對該屬性返回 false ;而其他的屬性(name 與 toString )則會正確地返回 true 。

使用 deleteProperty 陷阱函數(shù)避免屬性被刪除

deleteProperty 陷阱函數(shù)會在使用 delete 運算符去刪除對象屬性時下被調(diào)用,并且會被傳入兩個參數(shù):

  • trapTarget :需要刪除屬性的對象(即代理的目標(biāo)對象);
  • key :需要刪除的屬性的鍵(字符串類型或符號類型)。

Reflect.deleteProperty() 方法也接受這兩個參數(shù),并提供了 deleteProperty 陷阱函數(shù)的默認實現(xiàn)。你可以結(jié)合 Reflect.deleteProperty() 方法以及 deleteProperty 陷阱函數(shù),來修改 delete 運算符的行為。例如,能確保 value 屬性不被刪除:

let target = {
    name: "target",
    value: 42
};
let proxy = new Proxy(target, {
    deleteProperty(trapTarget, key) {
        if (key === "value") {
            return false;
        } else {
            return Reflect.deleteProperty(trapTarget, key);
        }
    }
});
// 嘗試刪除 proxy.value
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
// 嘗試刪除 proxy.name
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false

原型代理的陷阱函數(shù)

ES6 引入該方法用于對 ES5 的Object.getPrototypeOf() 方法進行補充。代理允許你通過 setPrototypeOf 與
getPrototypeOf 陷阱函數(shù)來對這兩個方法的操作進行攔截。Object對象上的這兩個方法都會調(diào)用代理中對應(yīng)名稱的陷阱函數(shù),從而允許你改變這兩個方法的行為。

setPrototypeOf 陷阱函數(shù)接受三個參數(shù):

  • trapTarget :需要設(shè)置原型的對象(即代理的目標(biāo)對象);
  • proto :需用被用作原型的對象。

getPrototypeOf 陷阱函數(shù)的返回值必須是一個對象或者null,其他任何類型的返回值都會引發(fā)“運行時”錯誤。對于返回值的檢測確保了Object.getPrototypeOf() 會返回預(yù)期的結(jié)果。類似的, setPrototypeOf 必須在操作沒有成功的情況下返回 false ,這樣會讓 Object.setPrototypeOf()拋出錯誤;而若setPrototypeOf的返回值不是false,則Object.setPrototypeOf() 就會認為操作已成功。

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return null;
    },
    setPrototypeOf(trapTarget, proto) {
        return false;
    }
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// 成功
Object.setPrototypeOf(target, {});
// 拋出錯誤
Object.setPrototypeOf(proxy, {});

如果你想在這兩個陷阱函數(shù)中使用默認的行為,那么只需調(diào)用Reflect對象上的相應(yīng)方法。例如,下面的代碼為getPrototypeOf 方法與 setPrototypeOf 方法實現(xiàn)了默認的行為:

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return Reflect.getPrototypeOf(trapTarget);
    },
    setPrototypeOf(trapTarget, proto) {
        return Reflect.setPrototypeOf(trapTarget, proto);
    }
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
// 成功
Object.setPrototypeOf(target, {});
// 同樣成功
Object.setPrototypeOf(proxy, {});

對象可擴展性的陷阱函數(shù)

ES5 通過 Object.preventExtensions() 與 Object.isExtensible() 方法給對象增加了可擴展性。而 ES6 則通過 preventExtensions 與 isExtensible 陷阱函數(shù)允許代理攔截對于底層對象的方法調(diào)用。

isExtensible 陷阱函數(shù)必須返回一個布爾值用于表明目標(biāo)對象是否可被擴展,而preventExtensions陷阱函數(shù)也需要返回一個布爾值,用于表明操作是否已成功。同時也存在 Reflect.preventExtensions() 與 Reflect.isExtensible() 方法,用于實現(xiàn)默認的行為。這兩個方法都返回布爾值,因此它們可以在對應(yīng)的陷阱函數(shù)內(nèi)直接使用。

為了弄懂對象可擴展性的陷阱函數(shù)如何運作,可研究如下代碼,該代碼實現(xiàn)了 isExtensible與 preventExtensions 陷阱函數(shù)的默認行為。

let target = {};
let proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return Reflect.preventExtensions(trapTarget);
    }
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false

屬性描述符的陷阱函數(shù)

ES5 最重要的特征之一就是引入了Object.defineProperty()方法用于定義屬性的特性。在JS之前的版本中,沒有方法可以定義一個訪問器屬性,也不能讓屬性變成只讀或是不可枚舉。而這些特性都能夠利用Object.defineProperty()方法來實現(xiàn),并且你還可以利用Object.getOwnPropertyDescriptor() 方法來檢索這些特性。

代理允許你使用 defineProperty 與 getOwnPropertyDescriptor 陷阱函數(shù),來分別攔截對Object.defineProperty() 與 Object.getOwnPropertyDescriptor() 的調(diào)用。 defineProperty陷阱函數(shù)接受下列三個參數(shù):

  • trapTarget :需要被定義屬性的對象(即代理的目標(biāo)對象);
  • key :屬性的鍵(字符串類型或符號類型);
  • descriptor :為該屬性準(zhǔn)備的描述符對象。

默認的行為:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        return Reflect.defineProperty(trapTarget, key, descriptor);
    },
    getOwnPropertyDescriptor(trapTarget, key) {
        return Reflect.getOwnPropertyDescriptor(trapTarget, key);
    }
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"

阻止 Object.defineProperty(

defineProperty 陷阱函數(shù)要求你返回一個布爾值用于表示操作是否已成功。當(dāng)它返回 true時, Object.defineProperty() 會正常執(zhí)行;而如果它返回了 false ,則Object.defineProperty() 會拋出錯誤。

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        if (typeof key === "symbol") {
            return false;
        }
        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// 拋出錯誤
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});

描述符對象的限制

為了確保 Object.defineProperty() 與 Object.getOwnPropertyDescriptor() 方法的行為一致,傳遞給 defineProperty 陷阱函數(shù)的描述符對象必須是正規(guī)的。出于同一原因,getOwnPropertyDescriptor陷阱函數(shù)返回的對象也始終需要被驗證。

任意對象都能作為 Object.defineProperty() 方法的第三個參數(shù);然而傳遞給defineProperty 陷阱函數(shù)的描述符對象參數(shù),則只有 enumerable 、 configurable 、value 、 writable 、 get 與 set 這些屬性是被許可的。

ownKeys 陷阱函數(shù)

ownKeys 代理陷阱攔截了內(nèi)部方法[[OwnPropertyKeys]],并允許你返回一個數(shù)組用于重寫該行為。返回的這個數(shù)組會被用于四個方法: Object.keys() 方法、Object.getOwnPropertyNames() 方法、 Object.getOwnPropertySymbols() 方法與
Object.assign() 方法,其中 Object.assign() 方法會使用該數(shù)組來決定哪些屬性會被復(fù)制。

ownKeys 陷阱函數(shù)的默認行為由Reflect.ownKeys()方法實現(xiàn),會返回一個由全部自有屬性的鍵構(gòu)成的數(shù)組,無論鍵的類型是字符串還是符號。 Object.getOwnProperyNames() 方法與Object.keys() 方法會將符號值從該數(shù)組中過濾出去;相反,
Object.getOwnPropertySymbols() 會將字符串值過濾掉;而Object.assign()方法會使用數(shù)組中所有的字符串值與符號值。

ownKeys 陷阱函數(shù)接受單個參數(shù),即目標(biāo)對象,同時必須返回一個數(shù)組或者一個類數(shù)組對象,不合要求的返回值會導(dǎo)致錯誤。你可以使用 ownKeys 陷阱函數(shù)去過濾特定的屬性,以避免這些屬性被Object.keys()方法、Object.getOwnPropertyNames() 方法、Object.getOwnPropertySymbols()方法或Object.assign()方法使用。假設(shè)你不想在結(jié)果中包含任何以下劃線打頭的屬性(在 JS 的編碼慣例中,這代表該字段是私有的),那么可以使用ownKeys陷阱函數(shù)來將它們過濾掉,就像下面這樣:

let proxy = new Proxy({}, {
    ownKeys(trapTarget) {
        return Reflect.ownKeys(trapTarget).filter(key => {
            return typeof key !== "string" || key[0] !== "_";
        });
    }
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy);
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "name"
console.log(keys.length); // 1
console.log(keys[0]); // "name"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"

ownKeys 陷阱函數(shù)也能影響 for-in 循環(huán),因為這種循環(huán)調(diào)用了陷阱函數(shù)來決定哪些值能夠被用在循環(huán)內(nèi)。

使用 apply 與 construct 陷阱函數(shù)的函數(shù)代理

在所有的代理陷阱中,只有 apply 與 construct 要求代理目標(biāo)對象必須是一個函數(shù)。函數(shù)擁有兩個內(nèi)部方法: [[Call]] 與 [[Construct]] ,前者會在函數(shù)被直接調(diào)用時執(zhí)行,而后者會在函數(shù)被使用 new 運算符調(diào)用時執(zhí)行。 apply 與 construct陷阱函數(shù)對應(yīng)著這兩個內(nèi)部方法,并允許你對其進行重寫。當(dāng)不使用 new 去調(diào)用一個函數(shù)時, apply 陷阱函數(shù)會接收到下列三個參數(shù)( Reflect.apply() 也會接收這些參數(shù)):

  • trapTarget :被執(zhí)行的函數(shù)(即代理的目標(biāo)對象);
  • thisArg :調(diào)用過程中函數(shù)內(nèi)部的 this 值;
  • argumentsList :被傳遞給函數(shù)的參數(shù)數(shù)組。

當(dāng)使用 new 去執(zhí)行函數(shù)時, construct 陷阱函數(shù)會被調(diào)用并接收到下列兩個參數(shù):

  • trapTarget :被執(zhí)行的函數(shù)(即代理的目標(biāo)對象);
  • argumentsList :被傳遞給函數(shù)的參數(shù)數(shù)組。

因此,可以用來做很多騷操作,比如

調(diào)用構(gòu)造器而無須使用 new

function Numbers(...values) {
    if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
    }
    this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
    apply: function(trapTarget, thisArg, argumentsList) {
        return Reflect.construct(trapTarget, argumentsList);
    }
});
let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]

可被撤銷的代理

在被創(chuàng)建之后,代理通常就不能再從目標(biāo)對象上被解綁。本章之前的例子都使用了不可被撤銷的代理,但有的情況下你可能想撤銷一個代理以便讓它不能再被使用。當(dāng)你想通過公共接口向外提供一個安全的對象,并且要求要隨時都能切斷對某些功能的訪問,這種情況下可被撤銷的代理就會非常有用。

你可以使用 Proxy.revocable()方法來創(chuàng)建一個可被撤銷的代理,該方法接受的參數(shù)與Proxy構(gòu)造器的相同:一個目標(biāo)對象- 、一個代理處理器,而返回值是包含下列屬性的一個對象:

  • proxy :可被撤銷的代理對象;
  • revoke :用于撤銷代理的函數(shù)。

當(dāng) revoke() 函數(shù)被調(diào)用后,就不能再對該proxy對象進行更多操作,任何與該代理對象交互的意圖都會觸發(fā)代理的陷阱函數(shù),從而拋出一個錯誤。

總結(jié)

在 ES6 之前,特定對象(例如數(shù)組)會顯示出一些非常規(guī)的、無法被開發(fā)者復(fù)制的行為,而代理的出現(xiàn)改變了這種情況。代理允許你為一些 JS 底層操作自行定義非常規(guī)行為,因此你就可以通過代理陷阱來復(fù)制JS內(nèi)置對象的所有行為。在各種不同操作發(fā)生時(例如對于 in運算符的使用),這些代理陷阱會在后臺被調(diào)用。

反射接口也是在 ES6 中引入的,允許開發(fā)者為每個代理陷阱實現(xiàn)默認的行為。每個代理陷阱在 Reflect 對象( ES6 的另一個新特性)上都有一個同名的對應(yīng)方法。將代理陷阱與反射接口方法結(jié)合使用,就可以在特定條件下讓一些操作有不同的表現(xiàn),有別于默認的內(nèi)置行為。

可被撤銷的代理是一種特殊的代理,可以使用revoke()函數(shù)去有效禁用。revoke()函數(shù)終結(jié)了代理的所有功能,因此在它被調(diào)用之后,所有與代理屬性交互的意圖都會導(dǎo)致拋出錯誤。第三方開發(fā)者可能需要在一定時間內(nèi)獲取特定對象的使用權(quán),在這種場合,可被撤銷的代理對應(yīng)用的安全性來說就非常重要。

盡管直接使用代理是最有力的使用方式,但你也可以把代理用作另一個對象的原型。但只有很少的代理陷阱能在作為原型的代理上被有效使用,包括 get 、 set 與 has 這幾個,這讓這方面的用例變得十分有限。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原創(chuàng)文章&經(jīng)驗總結(jié)&從校招到A廠一路陽光一路滄桑 詳情請戳www.codercc.com 主要知識點:代理和反射的...
    你聽___閱讀 2,005評論 0 1
  • 第一章:塊級作用域綁定 塊級聲明 1.var聲明及變量提升機制:在函數(shù)作用域或者全局作用域中通過關(guān)鍵字var聲明的...
    BeADre_wang閱讀 996評論 0 0
  • 本人自學(xué)es6已經(jīng)有一段時間了,只覺得有些時候很是枯燥無味, 時而又覺得在以后的職業(yè)生涯中會很有用,因為es6的很...
    可樂_37d3閱讀 1,659評論 0 0
  • 如果你平時有關(guān)注 Vue 的進展的話,可能已經(jīng)知道了在 Vue3.0 中將會通過 Proxy 來替換原本的 Obj...
    感覺不錯哦閱讀 924評論 0 1
  • 今日復(fù)盤 1,整理好關(guān)鍵詞表。 2,研究產(chǎn)品存在的問題 今日總結(jié) 1,目前對產(chǎn)品的熟悉程度不夠,需要多去了解產(chǎn)品的...
    Michele_7d15閱讀 77評論 0 0

友情鏈接更多精彩內(nèi)容