Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
能夠手撕各種JavaScript原生函數(shù),可以說(shuō)是進(jìn)大廠必備!同時(shí)對(duì)JavaScript源碼的學(xué)習(xí)和實(shí)現(xiàn)也能幫助我們快速扎實(shí)地提升自己的前端編程能力。
最近很多人和我一樣在積極地準(zhǔn)備前端面試筆試,所以就整理了一些前端面試筆試中非常容易被問(wèn)到的原生函數(shù)實(shí)現(xiàn)和各種前端原理實(shí)現(xiàn),其中部分源碼戳這里。
我們首先知道new做了什么:
知道new做了什么,接下來(lái)我們就來(lái)實(shí)現(xiàn)它
function create(Con, ...args){
// 創(chuàng)建一個(gè)空的對(duì)象
this.obj = {};
// 將空對(duì)象指向構(gòu)造函數(shù)的原型鏈
Object.setPrototypeOf(this.obj, Con.prototype);
// obj綁定到構(gòu)造函數(shù)上,便可以訪問(wèn)構(gòu)造函數(shù)中的屬性,即this.obj.Con(args)
let result = Con.apply(this.obj, args);
// 如果返回的result是一個(gè)對(duì)象則返回
// new方法失效,否則返回obj
return result instanceof Object ? result : this.obj;
}
思路很簡(jiǎn)單,就是利用Object.prototype.toString
Array.myIsArray = function(o) {
return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
function create = function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
真實(shí)經(jīng)歷,最近在字節(jié)跳動(dòng)的面試中就被面試官問(wèn)到了,要求手寫(xiě)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Event類(lèi)。
class Event {
constructor () {
// 儲(chǔ)存事件的數(shù)據(jù)結(jié)構(gòu)
// 為查找迅速, 使用對(duì)象(字典)
this._cache = {}
}
// 綁定
on(type, callback) {
// 為了按類(lèi)查找方便和節(jié)省空間
// 將同一類(lèi)型事件放到一個(gè)數(shù)組中
// 這里的數(shù)組是隊(duì)列, 遵循先進(jìn)先出
// 即新綁定的事件先觸發(fā)
let fns = (this._cache[type] = this._cache[type] || [])
if(fns.indexOf(callback) === -1) {
fns.push(callback)
}
return this
}
// 解綁
off (type, callback) {
let fns = this._cache[type]
if(Array.isArray(fns)) {
if(callback) {
let index = fns.indexOf(callback)
if(index !== -1) {
fns.splice(index, 1)
}
} else {
// 全部清空
fns.length = 0
}
}
return this
}
// 觸發(fā)emit
trigger(type, data) {
let fns = this._cache[type]
if(Array.isArray(fns)) {
fns.forEach((fn) => {
fn(data)
})
}
return this
}
// 一次性綁定
once(type, callback) {
let wrapFun = () => {
callback.call(this);
this.off(type, wrapFun); // 執(zhí)行完以后立即解綁
};
this.on(type, wrapFun); // 綁定
return this;
}
}
let e = new Event()
e.on('click',function(){
console.log('on')
})
// e.trigger('click', '666')
console.log(e)
先回憶一下Array.prototype.reduce語(yǔ)法:
Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
然后就可以動(dòng)手實(shí)現(xiàn)了:
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue ? initialValue : this[0];
for (let i = initialValue ? 0 : 1; i < this.length; i++) {
let _this = this;
accumulator = callback(accumulator, this[i], i, _this);
}
return accumulator;
};
// 使用
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((acc, val) => {
acc += val;
return acc;
}, 5);
console.log(sum); // 15
先來(lái)看一個(gè)call實(shí)例,看看call到底做了什么:
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
從代碼的執(zhí)行結(jié)果,我們可以看到,call首先改變了this的指向,使函數(shù)的this指向了foo,然后使bar函數(shù)執(zhí)行了。總結(jié)一下:
思考一下:我們?nèi)绾螌?shí)現(xiàn)上面的效果呢?代碼改造如下:
Function.prototype.myCall = function(context) {
context = context || window;
//將函數(shù)掛載到對(duì)象的fn屬性上
context.fn = this;
//處理傳入的參數(shù)
const args = [...arguments].slice(1);
//通過(guò)對(duì)象的屬性調(diào)用該方法
const result = context.fn(...args);
//刪除該屬性
delete context.fn;
return result
};
我們看一下上面的代碼:
以此類(lèi)推,我們順便實(shí)現(xiàn)一下apply,唯一不同的是參數(shù)的處理,代碼如下:
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this
let result
// myApply的參數(shù)形式為(obj,[arg1,arg2,arg3]);
// 所以myApply的第二個(gè)參數(shù)為[arg1,arg2,arg3]
// 這里我們用擴(kuò)展運(yùn)算符來(lái)處理一下參數(shù)的傳入方式
if (arguments[1]) {
result = context.fn(…arguments[1])
} else {
result = context.fn()
}
delete context.fn;
return result
};
以上便是call和apply的模擬實(shí)現(xiàn),唯一不同的是對(duì)參數(shù)的處理方式。
function Person(){
this.name="zs";
this.age=18;
this.gender="男"
}
let obj={
hobby:"看書(shū)"
}
// 將構(gòu)造函數(shù)的this綁定為obj
let changePerson = Person.bind(obj);
// 直接調(diào)用構(gòu)造函數(shù),函數(shù)會(huì)操作obj對(duì)象,給其添加三個(gè)屬性;
changePerson();
// 1、輸出obj
console.log(obj);
// 用改變了this指向的構(gòu)造函數(shù),new一個(gè)實(shí)例出來(lái)
let p = new changePerson();
// 2、輸出obj
console.log(p);
仔細(xì)觀察上面的代碼,再看輸出結(jié)果。
我們對(duì)Person類(lèi)使用了bind將其this指向obj,得到了changeperson函數(shù),此處如果我們直接調(diào)用changeperson會(huì)改變obj,若用new調(diào)用changeperson會(huì)得到實(shí)例 p,并且其__proto__指向Person,我們發(fā)現(xiàn)bind失效了。
我們得到結(jié)論:用bind改變了this指向的函數(shù),如果用new操作符來(lái)調(diào)用,bind將會(huì)失效。
這個(gè)對(duì)象就是這個(gè)構(gòu)造函數(shù)的實(shí)例,那么只要在函數(shù)內(nèi)部執(zhí)行 this instanceof 構(gòu)造函數(shù) 來(lái)判斷其結(jié)果是否為true,就能判斷函數(shù)是否是通過(guò)new操作符來(lái)調(diào)用了,若結(jié)果為true則是用new操作符調(diào)用的,代碼修正如下:
// bind實(shí)現(xiàn)
Function.prototype.mybind = function(){
// 1、保存函數(shù)
let _this = this;
// 2、保存目標(biāo)對(duì)象
let context = arguments[0]||window;
// 3、保存目標(biāo)對(duì)象之外的參數(shù),將其轉(zhuǎn)化為數(shù)組;
let rest = Array.prototype.slice.call(arguments,1);
// 4、返回一個(gè)待執(zhí)行的函數(shù)
return function F(){
// 5、將二次傳遞的參數(shù)轉(zhuǎn)化為數(shù)組;
let rest2 = Array.prototype.slice.call(arguments)
if(this instanceof F){
// 6、若是用new操作符調(diào)用,則直接用new 調(diào)用原函數(shù),并用擴(kuò)展運(yùn)算符傳遞參數(shù)
return new _this(...rest2)
}else{
//7、用apply調(diào)用第一步保存的函數(shù),并綁定this,傳遞合并的參數(shù)數(shù)組,即context._this(rest.concat(rest2))
_this.apply(context,rest.concat(rest2));
}
}
};
Currying的概念其實(shí)并不復(fù)雜,用通俗易懂的話(huà)說(shuō):只傳遞給函數(shù)一部分參數(shù)來(lái)調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)。
function progressCurrying(fn, args) {
let _this = this
let len = fn.length;
let args = args || [];
return function() {
let _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);
// 如果參數(shù)個(gè)數(shù)小于最初的fn.length,則遞歸調(diào)用,繼續(xù)收集參數(shù)
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args);
}
// 參數(shù)收集完畢,則執(zhí)行fn
return fn.apply(this, _args);
}
}
防抖函數(shù) onscroll 結(jié)束時(shí)觸發(fā)一次,延遲執(zhí)行
function debounce(func, wait) {
let timeout;
return function() {
let context = this; // 指向全局
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func.apply(context, args); // context.func(args)
}, wait);
};
}
// 使用
window.onscroll = debounce(function() {
console.log('debounce');
}, 1000);
節(jié)流函數(shù) onscroll 時(shí),每隔一段時(shí)間觸發(fā)一次,像水滴一樣
function throttle(fn, delay) {
let prevTime = Date.now();
return function() {
let curTime = Date.now();
if (curTime - prevTime > delay) {
fn.apply(this, arguments);
prevTime = curTime;
}
};
}
// 使用
var throtteScroll = throttle(function() {
console.log('throtte');
}, 1000);
window.onscroll = throtteScroll;
乞丐版
JSON.parse(JSON.stringfy));
非常簡(jiǎn)單,但缺陷也很明顯,比如拷貝其他引用類(lèi)型、拷貝函數(shù)、循環(huán)引用等情況。
基礎(chǔ)版
function clone(target){
if(typeof target === 'object'){
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = clone(target[key])
}
return cloneTarget;
} else {
return target
}
}
寫(xiě)到這里已經(jīng)可以幫助你應(yīng)付一些面試官考察你的遞歸解決問(wèn)題的能力。但是顯然,這個(gè)深拷貝函數(shù)還是有一些問(wèn)題。
一個(gè)比較完整的深拷貝函數(shù),需要同時(shí)考慮對(duì)象和數(shù)組,考慮循環(huán)引用:
function clone(target, map = new WeakMap()) {
if(typeof target === 'object'){
let cloneTarget = Array.isArray(target) ? [] : {};
if(map.get(target)) {
return target;
}
map.set(target, cloneTarget);
for(const key in target) {
cloneTarget[key] = clone(target[key], map)
}
return cloneTarget;
} else {
return target;
}
}
原理: L 的 proto 是不是等于 R.prototype,不等于再找 L.__proto__.__proto__ 直到 proto 為 null
// L 表示左表達(dá)式,R 表示右表達(dá)式
function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null){
return false;
}
// 這里重點(diǎn):當(dāng) O 嚴(yán)格等于 L 時(shí),返回 true
if (O === L) {
return true;
}
L = L.__proto__;
}
}
function myExtend(C, P) {
var F = function(){};
F.prototype = P.prototype;
C.prototype = new F();
C.prototype.constructor = C;
C.super = P.prototype;
}
就是利用 generator(生成器)分割代碼片段。然后我們使用一個(gè)函數(shù)讓其自迭代,每一個(gè)yield 用 promise 包裹起來(lái)。執(zhí)行下一步的時(shí)機(jī)由 promise 來(lái)控制
function _asyncToGenerator(fn) {
return function() {
var self = this,
args = arguments;
// 將返回值promise化
return new Promise(function(resolve, reject) {
// 獲取迭代器實(shí)例
var gen = fn.apply(self, args);
// 執(zhí)行下一步
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
// 拋出異常
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
// 第一次觸發(fā)
_next(undefined);
});
};
}
最近字節(jié)跳動(dòng)的前端面試中也被面試官問(wèn)到,要求手寫(xiě)實(shí)現(xiàn)。
Array.prototype.myFlat = function(num = 1) {
if (Array.isArray(this)) {
let arr = [];
if (!Number(num) || Number(num) < 0) {
return this;
}
this.forEach(item => {
if(Array.isArray(item)){
let count = num
arr = arr.concat(item.myFlat(--count))
} else {
arr.push(item)
}
});
return arr;
} else {
throw tihs + ".flat is not a function";
}
};
這個(gè)問(wèn)題一般還會(huì)讓你講一講事件冒泡和事件捕獲機(jī)制
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>
<script>
(function () {
var color_list = document.getElementById('color-list');
color_list.addEventListener('click', showColor, true);
function showColor(e) {
var x = e.target;
if (x.nodeName.toLowerCase() === 'li') {
alert(x.innerHTML);
}
}
})();
</script>
Vue 2.x的Object.defineProperty版本
// 數(shù)據(jù)
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 數(shù)據(jù)劫持
Object.defineProperty(data, 'text', {
// 數(shù)據(jù)變化 —> 修改視圖
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});
// 視圖更改 --> 數(shù)據(jù)變化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
Vue 3.x的proxy 版本
// 數(shù)據(jù)
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 數(shù)據(jù)劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 數(shù)據(jù)變化 —> 修改視圖
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);
// 視圖更改 --> 數(shù)據(jù)變化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
思考:Vue雙向綁定的實(shí)現(xiàn),使用 ES6 的 Proxy 相比 Object.defineProperty 有什么優(yōu)勢(shì)?
先看看reduce和map的使用方法
let new_array = arr.map(function callback(currentValue[, index[,array) {}[, thisArg])
let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
最常見(jiàn)的方式我們可以用一個(gè)for循環(huán)來(lái)實(shí)現(xiàn):
Array.prototype.myMap = function(callback, thisArg) {
let arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(callback.call(thisArg, this[i], i, this));
}
return arr;
};
同樣的我們也可以用數(shù)組的reduce方法實(shí)現(xiàn)
Array.prototype.myMap2 = function(callback, thisArg) {
let result = this.reduce((accumulator, currentValue, index, array) => {
accumulator.push(callback.call(thisArg, currentValue, index, array));
return accumulator;
}, []);
return result;
};
看完如果覺(jué)得對(duì)你有幫助,勞煩點(diǎn)個(gè)贊哈,你的鼓勵(lì)就是我更新最大的動(dòng)力!
學(xué)習(xí)使我快樂(lè)!
么是函數(shù)?
把一段相對(duì)獨(dú)立的具有特定功能的代碼塊封裝起來(lái),形成一個(gè)獨(dú)立實(shí)體,就是函數(shù),起個(gè)名字(函數(shù)名),在后續(xù)開(kāi)發(fā)中可以反復(fù)調(diào)用。函數(shù)的作用就是封裝一段代碼,將來(lái)可以重復(fù)使用。推薦了解黑馬程序員web前端課程。
為什么要使用函數(shù)?為了解決代碼的重用!減少代碼量。
函數(shù)的分類(lèi)
系統(tǒng)內(nèi)置函數(shù) 和 程序員自定義函數(shù)
定義函數(shù)
function 函數(shù)名([參數(shù)列表]){
函數(shù)體
}
結(jié)構(gòu)說(shuō)明:
·function它是定義函數(shù)的關(guān)鍵字 不可以省略。
·函數(shù)名它的命名規(guī)則與變量名是一樣的
·函數(shù)名后面緊跟著一對(duì)小括號(hào) 這一對(duì)小括號(hào)不能省略
·小括號(hào)里面可能有參數(shù),我們將其稱(chēng)之為形參
·小括號(hào)后面緊跟著一對(duì)大括號(hào) 這一對(duì)大括號(hào)不能省略
·大括號(hào)里面是函數(shù)體
注意:函數(shù)定義了一定要調(diào)用函數(shù)
調(diào)用函數(shù)
格式:
函數(shù)名([實(shí)參列表])
注意:在定義函數(shù)時(shí)如果有形參 反過(guò)來(lái)說(shuō) 在調(diào)用的時(shí)候也要有實(shí)參 但是這個(gè)不是絕對(duì)的!
同名函數(shù)后面的會(huì)將前面的覆蓋
函數(shù)一定是先定義后再調(diào)用
函數(shù)的參數(shù)
函數(shù)的參數(shù)分為兩種:
形式參數(shù)和實(shí)際參數(shù)
形式參數(shù):在定義函數(shù)的時(shí)候的參數(shù)就稱(chēng)之為形式參數(shù),簡(jiǎn)稱(chēng)“形參”。在定義函數(shù)的時(shí)候 在函數(shù)名的后面的小括號(hào)里面給的變量名。
實(shí)際參數(shù):在調(diào)用函數(shù)的時(shí)候的參數(shù)就稱(chēng)之為實(shí)際參數(shù),簡(jiǎn)稱(chēng)“實(shí)參”。
在一個(gè)函數(shù)中,參數(shù)的多少是根據(jù)功能來(lái)定義的!
使用函數(shù)來(lái)動(dòng)態(tài)的輸出M行N列的表格
一般在函數(shù)體里面不會(huì)有輸出語(yǔ)句,只會(huì)有一個(gè)return關(guān)鍵字,將我們要輸出的內(nèi)容返回給函數(shù)的調(diào)用者。
·return在英文中表示“返回”的意思
·return關(guān)鍵字是在函數(shù)體里面使用。
它在函數(shù)體使用有兩層含義:
2.它會(huì)向函數(shù)的調(diào)用者返回?cái)?shù)據(jù)(重點(diǎn))返回值
格式:return數(shù)據(jù);
在調(diào)用函數(shù)時(shí)可以定義一個(gè)變量要接收到這個(gè)函數(shù)的返回值
注意:
我們?cè)谝院蟮墓ぷ髦校瘮?shù)體里面盡量不要有輸出語(yǔ)句(document.write alert console.log ),盡量的使用return關(guān)鍵字將數(shù)據(jù)返回給函數(shù)的調(diào)用者。
特別強(qiáng)調(diào):
·在一個(gè)函數(shù)里面,return關(guān)鍵字可以有多個(gè),但是return只會(huì)執(zhí)行一次;
·return關(guān)鍵字它只能返回一個(gè)數(shù)據(jù),如果需要返回多個(gè)數(shù)據(jù),我們可以將多個(gè)數(shù)據(jù)使用數(shù)組的方式來(lái)保存,然后再將數(shù)組返回。
匿名函數(shù)
什么是匿名函數(shù)?
沒(méi)有名字的函數(shù) 稱(chēng)之為匿名函數(shù)!注意:匿名函數(shù)在JS中使用最多。
匿名函數(shù)也是需要調(diào)用的!
將匿名函數(shù)賦值給變量或者是事件
a)將匿名函數(shù)賦值給變量,然后通過(guò)變量名加上小括號(hào)來(lái)調(diào)用匿名函數(shù)
b)將匿名函數(shù)賦值給事件 *****
將匿名函數(shù)賦值給事件,那么匿名函數(shù)什么時(shí)候才會(huì)執(zhí)行?它要等到事件觸發(fā)了以后,匿名函數(shù)才會(huì)執(zhí)行。
什么是變量的作用域?
指變量在什么地方可以使用,什么地方不可以使用。
變量作用域的分類(lèi)
變量作用域分為:全局作用域和局部作用域。
變量的作用域是通過(guò)函數(shù)來(lái)進(jìn)行劃分的。
在函數(shù)外面定義的變量我們將其稱(chēng)為全局變量,它的作用域是全局的。
全局作用域: 在任何地方都可以訪問(wèn)到的變量就是全局變量,對(duì)應(yīng)全局作用域
局部作用域: 在固定的代碼片段內(nèi)可訪問(wèn)到的變量,最常見(jiàn)的例如函數(shù)內(nèi)部。對(duì)應(yīng)局部作用域(函數(shù)作用域)
問(wèn):是否能夠提升局部變量的作用域呢?將局部變量的作用域提升至全局作用域。在函數(shù)里面定義的變量也能夠在函數(shù)外面訪問(wèn)到。
只需要將函數(shù)里面定義的變量的var關(guān)鍵字給去掉就可以實(shí)現(xiàn)將局部變量的作用域提升至全局作用域。
但是:并不建議直接就var 關(guān)鍵字給省略,我們建議在函數(shù)的外面定義一個(gè)同名的全局變量。
擊標(biāo)題下「異步社區(qū)」可快速關(guān)注
本文包括以下內(nèi)容:
理解函數(shù)為何如此重要
函數(shù)為何是第一類(lèi)對(duì)象
定義函數(shù)的方式
參數(shù)賦值之謎
在本文這一部分討論JavaScript基礎(chǔ)時(shí),也許你會(huì)感到驚訝,我們的第一個(gè)論點(diǎn)是函數(shù)(function)而非對(duì)象(object)。當(dāng)然,第3部分會(huì)用大量筆墨解釋對(duì)象,但歸根結(jié)底,你要理解一些基本事實(shí),像普通人一樣編寫(xiě)代碼和像“忍者”一樣編寫(xiě)代碼的最大差別在于是否把JavaScript作為函數(shù)式語(yǔ)言(functional language)來(lái)理解。對(duì)這一點(diǎn)的認(rèn)知水平?jīng)Q定了你編寫(xiě)的代碼水平。
如果你正在閱讀這本文,那么你應(yīng)該不是一位初學(xué)者。對(duì)于后續(xù)內(nèi)容,我們假設(shè)你已經(jīng)足夠了解面向?qū)ο蠡A(chǔ)(當(dāng)然,我們會(huì)在以后章節(jié)詳細(xì)討論對(duì)象的高級(jí)概念),但真正理解JavaScript中的函數(shù)才是你能使用的唯一一件重要武器。函數(shù)是如此重要,所以本文及接下來(lái)兩章將帶領(lǐng)你徹底理解JavaScript中的函數(shù)。
JavaScript中最關(guān)鍵的概念是:函數(shù)是第一類(lèi)對(duì)象(first-class objects),或者說(shuō)它們被稱(chēng)作一等公民(first-class citizens)。函數(shù)與對(duì)象共存,函數(shù)也可以被視為其他任意類(lèi)型的JavaScript對(duì)象。函數(shù)和那些更普通的JavaScript數(shù)據(jù)類(lèi)型一樣,它能被變量引用,能以字面量形式聲明,甚至能被作為函數(shù)參數(shù)進(jìn)行傳遞。本文一開(kāi)始會(huì)介紹面向函數(shù)編程帶來(lái)的差異,你會(huì)發(fā)現(xiàn),在需要調(diào)用某函數(shù)的位置定義該函數(shù),能讓我們編寫(xiě)更緊湊、更易懂的代碼。其次,我們還會(huì)探索如何把函數(shù)用作第一類(lèi)對(duì)象來(lái)編寫(xiě)高性能函數(shù)。你能學(xué)到多種不同的函數(shù)定義方式,甚至包括一些新類(lèi)型,例如箭頭(arrow)函數(shù),它能幫你編寫(xiě)更優(yōu)雅的代碼。最后,我們會(huì)學(xué)習(xí)函數(shù)形參和函數(shù)實(shí)參的區(qū)別,并重點(diǎn)關(guān)注ES6的新增特性,例如剩余參數(shù)和默認(rèn)參數(shù)。
讓我們通過(guò)了解函數(shù)式編程的優(yōu)點(diǎn)來(lái)開(kāi)始學(xué)習(xí)吧。
你知道嗎?
回調(diào)函數(shù)在哪種情況下會(huì)同步調(diào)用,或者異步調(diào)用呢?
箭頭函數(shù)和函數(shù)表達(dá)式的區(qū)別是什么?
你為什么需要在函數(shù)中使用默認(rèn)參數(shù)?
1.1 函數(shù)式的不同點(diǎn)到底是什么
函數(shù)及函數(shù)式概念之所以如此重要,其原因之一在于函數(shù)是程序執(zhí)行過(guò)程中的主要模塊單元。除了全局JavaScript代碼是在頁(yè)面構(gòu)建的階段執(zhí)行的,我們編寫(xiě)的所有的腳本代碼都將在一個(gè)函數(shù)內(nèi)執(zhí)行。
由于我們的大多數(shù)代碼會(huì)作為函數(shù)調(diào)用來(lái)執(zhí)行,因此,我們?cè)诰帉?xiě)代碼時(shí),通用強(qiáng)大的構(gòu)造器能賦予代碼很大的靈活性和控制力。本文的大部分內(nèi)容解釋了如何利用函數(shù)作為第一類(lèi)對(duì)象的特性獲益。首先瀏覽一下對(duì)象中我們能使用的功能。JavaScript中對(duì)象有以下幾種常用功能。
對(duì)象可通過(guò)字面量來(lái)創(chuàng)建{}。
對(duì)象可以賦值給變量、數(shù)組項(xiàng),或其他對(duì)象的屬性。
1var ninja = {}; ?--- 為變量賦值一個(gè)新對(duì)象
2ninjaArray.push({}); ?--- 向數(shù)組中增加一個(gè)新對(duì)象
3ninja.data = {}; ?--- 給某個(gè)對(duì)象的屬性賦值為一個(gè)新對(duì)象
對(duì)象可以作為參數(shù)傳遞給函數(shù)。
1function hide(ninja){
2 ninja.visibility = false;
3}
4hide({}); ?--- 一個(gè)新創(chuàng)建的對(duì)象作為參數(shù)傳遞給函數(shù)
對(duì)象可以作為函數(shù)的返回值。
1function returnNewNinja() {
2 return {}; ?--- 從函數(shù)中返回了一個(gè)新對(duì)象
3}
對(duì)象能夠具有動(dòng)態(tài)創(chuàng)建和分配的屬性。
1var ninja = {};
2ninja.name = "Hanzo"; ?--- 為對(duì)象分配一個(gè)新屬性
其實(shí),不同于很多其他編程語(yǔ)言,在JavaScript中,我們幾乎能夠用函數(shù)來(lái)實(shí)現(xiàn)同樣的事。
1.1.1 函數(shù)是第一類(lèi)對(duì)象
JavaScript中函數(shù)擁有對(duì)象的所有能力,也因此函數(shù)可被作為任意其他類(lèi)型對(duì)象來(lái)對(duì)待。當(dāng)我們說(shuō)函數(shù)是第一類(lèi)對(duì)象的時(shí)候,就是說(shuō)函數(shù)也能夠?qū)崿F(xiàn)以下功能。
通過(guò)字面量創(chuàng)建。
1function ninjaFunction() {}
賦值給變量,數(shù)組項(xiàng)或其他對(duì)象的屬性。
1var ninjaFunction = function() {}; ?--- 為變量賦值一個(gè)新函數(shù)
2ninjaArray.push(function(){}); ?--- 向數(shù)組中增加一個(gè)新函數(shù)
3ninja.data = function(){}; ?--- 給某個(gè)對(duì)象的屬性賦值為一個(gè)新函數(shù)
作為函數(shù)的參數(shù)來(lái)傳遞。
1function call(ninjaFunction){
2 ninjaFunction();
3}
4call(function(){}); ?--- 一個(gè)新函數(shù)作為參數(shù)傳遞給函數(shù)
作為函數(shù)的返回值。
1function returnNewNinjaFunction() {
2 return function(){}; ?--- 返回一個(gè)新函數(shù)
3}
具有動(dòng)態(tài)創(chuàng)建和分配的屬性。
1var ninjaFunction = function(){};
2ninjaFunction.ninja = "Hanzo"; ?--- 為函數(shù)增加一個(gè)新屬性
對(duì)象能做的任何一件事,函數(shù)也都能做。函數(shù)也是對(duì)象,唯一的特殊之處在于它是可調(diào)用的(invokable),即函數(shù)會(huì)被調(diào)用以便執(zhí)行某項(xiàng)動(dòng)作。
{JavaScript中的函數(shù)式編程!}
把函數(shù)作為第一類(lèi)對(duì)象是函數(shù)式編程(functional programming)的第一步,函數(shù)式編程是一種編程風(fēng)格,它通過(guò)書(shū)寫(xiě)函數(shù)式(而不是指定一系列執(zhí)行步驟,就像那種更主流的命令式編程)代碼來(lái)解決問(wèn)題。函數(shù)式編程可以讓代碼更容易測(cè)試、擴(kuò)展及模塊化。不過(guò)這是一個(gè)很大的話(huà)題,因此本文僅對(duì)這個(gè)特性做了肯定。如果你對(duì)如何在JavacScript中利用函數(shù)式編程感興趣,推薦閱讀Luis Atencio著(由Manning出版社2016年出版)的《JavaScript函數(shù)式編程》,購(gòu)買(mǎi)方式見(jiàn)www.manning.com/ books/functional-programming- in-JavaScript。
第一類(lèi)對(duì)象的特點(diǎn)之一是,它能夠作為參數(shù)傳入函數(shù)。對(duì)于函數(shù)而言,這項(xiàng)特性也表明:如果我們將某個(gè)函數(shù)作為參數(shù)傳入另一個(gè)函數(shù),傳入函數(shù)會(huì)在應(yīng)用程序執(zhí)行的未來(lái)某個(gè)時(shí)間點(diǎn)才執(zhí)行。大家所知道的更一般的概念是回調(diào)函數(shù)(callback function)。下面我們來(lái)學(xué)習(xí)這個(gè)重要概念。
1.1.2 回調(diào)函數(shù)
每當(dāng)我們建立了一個(gè)將在隨后調(diào)用的函數(shù)時(shí),無(wú)論是在事件處理階段通過(guò)瀏覽器還是通過(guò)其他代碼,我們都是在建立一個(gè)回調(diào)(callback)。這個(gè)術(shù)語(yǔ)源自于這樣一個(gè)事實(shí),即在執(zhí)行過(guò)程中,我們建立的函數(shù)會(huì)被其他函數(shù)在稍后的某個(gè)合適時(shí)間點(diǎn)“再回來(lái)調(diào)用”。
有效運(yùn)用JavaScript的關(guān)鍵在于回調(diào)函數(shù),相信你已經(jīng)在代碼中使用了很多回調(diào)函數(shù)——不論是單擊一次按鈕、從服務(wù)端接收數(shù)據(jù),還是UI動(dòng)畫(huà)的一部分。
本節(jié)我們會(huì)看一些實(shí)際使用回調(diào)函數(shù)的典型例子,例如處理事件、簡(jiǎn)單的排序集合。這部分內(nèi)容會(huì)有點(diǎn)復(fù)雜,所以在深入學(xué)習(xí)之前,先透徹、完整地理解回調(diào)函數(shù)的概念,用最簡(jiǎn)單的形式來(lái)展現(xiàn)它。下面我們用一個(gè)簡(jiǎn)單例子來(lái)闡明這個(gè)概念,此例中的函數(shù)完全沒(méi)什么實(shí)際用處,它的參數(shù)接收另一個(gè)函數(shù)的引用,并作為回調(diào)調(diào)用該函數(shù):
1function useless(ninjaCallback) {
2 return ninjaCallback();
3}
這個(gè)函數(shù)可能沒(méi)什么用,但它反映了函數(shù)的一種能力,即將函數(shù)作為另一個(gè)函數(shù)的參數(shù),隨后通過(guò)參數(shù)來(lái)調(diào)用該函數(shù)。
我們可以在清單1.1中測(cè)試一下這個(gè)名為useless的函數(shù)。
清單1.1 簡(jiǎn)單的回調(diào)函數(shù)例子
1var text = "Domo arigato!";
2report("Before defining functions");
3function useless(ninjaCallback) {
4 report("In useless function");
5 return ninjaCallback();
6} ?--- 函數(shù)定義,參數(shù)為一個(gè)回調(diào)函數(shù),其函數(shù)體內(nèi)會(huì)立即調(diào)用該回調(diào)函數(shù)
7function getText() {
8 report("In getText function");
9 return text;
10} ?--- 簡(jiǎn)單的函數(shù)定義,僅返回一個(gè)全局變量
11report("Before making all the calls");
12assert(useless(getText) === text,
13 "The useless function works! " + text); ?--- 把gerText作為回調(diào)函數(shù)傳入上面的useless函數(shù)
14report("After the calls have been made");
在這個(gè)代碼清單中,我們使用自定義函數(shù)report(在本文附錄B中定義)來(lái)輸出代碼執(zhí)行過(guò)程中的信息,這樣一來(lái)我們就能通過(guò)這些信息來(lái)跟蹤程序的執(zhí)行過(guò)程。我們還使用了第1章中的斷言函數(shù)assert。該函數(shù)通常使用兩個(gè)參數(shù)。第一個(gè)參數(shù)是用于斷言的表達(dá)式。本例中,我們需要確定使用參數(shù)getText調(diào)用useless函數(shù)返回的值與變量text是否相等(useless(getText) === text)。若第一個(gè)參數(shù)的執(zhí)行結(jié)果為true,斷言通過(guò);反之,斷言失敗。第二個(gè)參數(shù)是與斷言相關(guān)聯(lián)的信息,通常會(huì)根據(jù)通過(guò)/失敗來(lái)輸出到日志上。(附錄B中概括地探討了測(cè)試,以及我們對(duì)assert函數(shù)和report函數(shù)的簡(jiǎn)單實(shí)現(xiàn))。
這段代碼執(zhí)行完畢后,執(zhí)行結(jié)果如圖1.1所示。可以看到,使用getText參數(shù)調(diào)用useless回調(diào)函數(shù)后,得到了期望的返回值。
圖1.1 清單1.1中代碼的執(zhí)行結(jié)果
我們還可以看看這個(gè)簡(jiǎn)單的回調(diào)函數(shù)具體是如何執(zhí)行的。如圖1.2所示,getText函數(shù)作為參數(shù)傳入了useless函數(shù)。從該圖中可以看到,在useless函數(shù)體內(nèi),通過(guò)callback參數(shù)可以取得getText函數(shù)的引用。隨后,回調(diào)函數(shù)callback()的調(diào)用讓getText函數(shù)得到執(zhí)行,而我們作為參數(shù)傳入的getText函數(shù)則通過(guò)useless函數(shù)被回調(diào)。
圖1.2 執(zhí)行useless(getText)調(diào)用后的執(zhí)行流。getText作為參數(shù)傳入useless函數(shù)并調(diào)用。useless函數(shù)體內(nèi)對(duì)傳入函數(shù)進(jìn)行調(diào)用,本例中觸發(fā)了getText函數(shù)的執(zhí)行(即我們對(duì)getText函數(shù)進(jìn)行回調(diào))。
完成這個(gè)過(guò)程是很容易的,原因就在于JavaScript的函數(shù)式本質(zhì)讓我們能把函數(shù)作為第一類(lèi)對(duì)象。更進(jìn)一步說(shuō),我們的代碼可以寫(xiě)成如下形式:
1<pre class="代碼無(wú)行號(hào)"><code>var text = 'Domo arigato!';
2function useless(ninjaCallback) {
3 return ninjaCallback();
4}
5assert(useless(<strong>function () { return text;}</strong>) === text, ?--- 直接以參數(shù)形式定義回調(diào)函數(shù)
6 "The useless function works! " + text); </code></pre>
JavaScript的重要特征之一是可以在表達(dá)式出現(xiàn)的任意位置創(chuàng)建函數(shù),除此之外這種方式能使代碼更緊湊和易于理解(把函數(shù)定義放在函數(shù)使用處附近)。當(dāng)一個(gè)函數(shù)不會(huì)在代碼的多處位置被調(diào)用時(shí),該特性可以避免用非必須的名字污染全局命名空間。
在回調(diào)函數(shù)的前述例子中,我們調(diào)用的是我們自己的回調(diào)。除此之外瀏覽器也會(huì)調(diào)用回調(diào)函數(shù),回想一下第2章中的下述例子:
1document.body.addEventListener("mousemove", function() {
2
3 var second = document.getElementById("second")
4;
5 addMessage(second, "Event: mousemove"
6);
7});
上例同樣是一個(gè)回調(diào)函數(shù),作為mousemove事件的事件處理器,當(dāng)事件發(fā)生時(shí),會(huì)被瀏覽器調(diào)用。
{注意 }
本小節(jié)介紹的回調(diào)函數(shù)是其他代碼會(huì)在隨后的某個(gè)合適時(shí)間點(diǎn)“回過(guò)來(lái)調(diào)用”的函數(shù)。你已經(jīng)學(xué)習(xí)了我們自己的代碼調(diào)用回調(diào)(useless函數(shù)例子),也看到了當(dāng)某事件發(fā)生時(shí)瀏覽器發(fā)起調(diào)用(mousemove例子)。注意這些很重要,不同于我們的例子,一些人認(rèn)為回調(diào)會(huì)被異步調(diào)用,因此第一個(gè)例子不是一個(gè)真正的回調(diào)。這里之所以提到這些是以防萬(wàn)一你偶爾會(huì)遇見(jiàn)這類(lèi)激烈的爭(zhēng)論。
現(xiàn)在讓我們看一個(gè)回調(diào)函數(shù)的用法,它能極大地簡(jiǎn)化集合的排序。
使用比較器排序
一般情況下只要我們拿到了一組數(shù)據(jù)集,就很可能需要對(duì)它進(jìn)行排序。假如有一組隨機(jī)序列的數(shù)字?jǐn)?shù)組:0, 3, 2, 5, 7, 4, 8, 1。也許這個(gè)順序沒(méi)什么問(wèn)題,但很可能早晚需要重新排列它。
通常來(lái)說(shuō),實(shí)現(xiàn)排序算法并不是編程任務(wù)中最微不足道的;我們需要為手中的工作選擇最佳算法,實(shí)現(xiàn)它以適應(yīng)當(dāng)前的需要(使這些選項(xiàng)是按照特定順序排列),并且需要小心仔細(xì)不能引入故障。除此之外,唯一特定于應(yīng)用程序的任務(wù)是排列順序。幸運(yùn)的是,所有的JavaScript數(shù)組都能用sort方法。利用該方法可以只定義一個(gè)比較算法,比較算法用于指示按什么順序排列。
這才是回調(diào)函數(shù)所要介入的!不同于讓排序算法來(lái)決定哪個(gè)值在前哪個(gè)值在后,我們將會(huì)提供一個(gè)函數(shù)來(lái)執(zhí)行比較。我們會(huì)讓排序算法能夠獲取這個(gè)比較函數(shù)作為回調(diào),使算法在其需要比較的時(shí)候,每次都能夠調(diào)用回調(diào)。該回調(diào)函數(shù)的期望返回值為:如果傳入值的順序需要被調(diào)換,返回正數(shù);不需要調(diào)換,返回負(fù)數(shù);兩個(gè)值相等,返回0。對(duì)于排序上述數(shù)組,我們對(duì)比較值做減法就能得到我們所需要的值。
1<pre class="代碼無(wú)行號(hào)"><code>var values = [0, 3, 2, 5, 7, 4, 8, 1];
2values.sort<strong>(function(value1, value2) {</strong>
3 return value1 - value2;
4<strong>}</strong>);</code></pre>
沒(méi)有必要思考排序算法的底層細(xì)節(jié)(甚至是選擇了什么算法)。JavaScript引擎每次需要比較兩個(gè)值的時(shí)候都會(huì)調(diào)用我們提供的回調(diào)函數(shù)。
函數(shù)式方式讓我們能把函數(shù)作為一個(gè)單獨(dú)實(shí)體來(lái)創(chuàng)建,正像我們對(duì)待其他類(lèi)型一樣,創(chuàng)建它、作為參數(shù)傳入一個(gè)方法并將它作為一個(gè)參數(shù)來(lái)接收。函數(shù)就這樣顯示了它一等公民的地位。
1.2 函數(shù)作為對(duì)象的樂(lè)趣
本節(jié)我們會(huì)考察函數(shù)和其他對(duì)象類(lèi)型的相似之處。也許讓你感到驚訝的相似之處在于我們可以給函數(shù)添加屬性:
1var ninja = {};
2ninja.name = "hitsuke"; ?--- 創(chuàng)建新對(duì)象并為其分配一個(gè)新屬性
3var wieldSword = function(){};
4wieldSword.swordType = "katana"; ?--- 創(chuàng)建新函數(shù)并為其分配一個(gè)新屬性
我們?cè)賮?lái)看看這種特性所能做的更有趣的事:
在集合中存儲(chǔ)函數(shù)使我們輕易管理相關(guān)聯(lián)的函數(shù)。例如,某些特定情況下必須調(diào)用的回調(diào)函數(shù)。
記憶讓函數(shù)能記住上次計(jì)算得到的值,從而提高后續(xù)調(diào)用的性能。
讓我們行動(dòng)起來(lái)吧。
1.2.1 存儲(chǔ)函數(shù)
某些例子中(例如,我們需要管理某個(gè)事件發(fā)生后需要調(diào)用的回調(diào)函數(shù)集合),我們會(huì)存儲(chǔ)元素唯一的函數(shù)集合。當(dāng)我們向這樣的集合中添加函數(shù)時(shí),會(huì)面臨兩個(gè)問(wèn)題:哪個(gè)函數(shù)對(duì)于這個(gè)集合來(lái)說(shuō)是一個(gè)新函數(shù),從而需要被加入到該集合中?又是哪個(gè)函數(shù)已經(jīng)存在于集合中,從而不需要再次加入到集合中?一般來(lái)說(shuō),管理回調(diào)函數(shù)集合時(shí),我們并不希望存在重復(fù)函數(shù),否則一個(gè)事件會(huì)導(dǎo)致同一個(gè)回調(diào)函數(shù)被多次調(diào)用。
一種顯著有效的簡(jiǎn)單方法是把所有函數(shù)存入一個(gè)數(shù)組,通過(guò)循環(huán)該數(shù)組來(lái)檢查重復(fù)函數(shù)。令人遺憾的是,這種方法的性能較差,尤其作為一個(gè)“忍者”要把事情干得漂亮而不僅是做到能用。我們可以使用函數(shù)的屬性,用適當(dāng)?shù)膹?fù)雜度來(lái)實(shí)現(xiàn)它,如清單1.2所示。
清單1.2 存儲(chǔ)唯一函數(shù)集合
1var store = {
2 nextId: 1, ?--- 跟蹤下一個(gè)要被復(fù)制的函數(shù)
3 cache: {}, ?--- 使用一個(gè)對(duì)象作為緩存,我們可以在其中存儲(chǔ)函數(shù)
4 add: function(fn) {
5 if (!fn.id) {
6 fn.id = this.nextId++;
7 this.cache[fn.id] = fn;
8 return true;
9 }
10 } ?--- 僅當(dāng)函數(shù)唯一時(shí),將該函數(shù)加入緩存
11};
12function ninja(){}
13assert(store.add(ninja),
14 "Function was safely added.");
15assert(!store.add(ninja),
16 "But it was only added once."); ?--- 測(cè)試上面代碼按預(yù)期工作
在這個(gè)清單中,我們創(chuàng)建了一個(gè)對(duì)象賦值給變量store,這個(gè)變量中存儲(chǔ)的是唯一的函數(shù)集合。這個(gè)對(duì)象有兩個(gè)數(shù)據(jù)屬性:其一是下一個(gè)可用的id,另外一個(gè)緩存著已經(jīng)保存的函數(shù)。函數(shù)通過(guò)add()方法添加到緩存中。
1add: function(fn) {
2 if (!fn.id) {
3 fn.id = this.nextId++;
4 this.cache[fn.id] = fn;
5 return true;
6 }
7...
在add函數(shù)內(nèi),我們首先檢查該函數(shù)是否已經(jīng)存在id屬性。如果當(dāng)前的函數(shù)已經(jīng)有id屬性,我們則假設(shè)該函數(shù)已經(jīng)被處理過(guò)了,從而忽略該函數(shù),否則為該函數(shù)分配一個(gè)id(同時(shí)增加nextId)屬性,并將該函數(shù)作為一個(gè)屬性增加到cache上,id作為屬性名。緊接著該函數(shù)的返回值為true,從而可得知調(diào)用了add()后,函數(shù)是什么時(shí)候被添加到存儲(chǔ)中的。
在瀏覽器中運(yùn)行該程序后,頁(yè)面顯示:測(cè)試程序嘗試兩次添加ninja()函數(shù),而該函數(shù)只被添加一次到存儲(chǔ)中,如圖1.3所示。第9章展示了用于操作合集的更好技術(shù),它利用了ES6的新的對(duì)象類(lèi)型集合(Set)。
圖1.3 給函數(shù)附加一個(gè)屬性后,我們就能夠引用該屬性。本例通過(guò)這種方式可以確保該ninja函數(shù)僅被添加到函數(shù)中一次
另外一種有用的技巧是當(dāng)使用函數(shù)屬性時(shí),可以通過(guò)該屬性修改函數(shù)自身。這個(gè)技術(shù)可以用于記憶前一個(gè)計(jì)算得到的值,為之后計(jì)算節(jié)省時(shí)間。
1.2.2 自記憶函數(shù)
如同前面所提到的,記憶化(memoization)是一種構(gòu)建函數(shù)的處理過(guò)程,能夠記住上次計(jì)算結(jié)果。在這個(gè)果殼里,當(dāng)函數(shù)計(jì)算得到結(jié)果時(shí)就將該結(jié)果按照參數(shù)存儲(chǔ)起來(lái)。采用這種方式時(shí),如果另外一個(gè)調(diào)用也使用相同的參數(shù),我們則可以直接返回上次存儲(chǔ)的結(jié)果而不是再計(jì)算一遍。像這樣避免既重復(fù)又復(fù)雜的計(jì)算可以顯著地提高性能。對(duì)于動(dòng)畫(huà)中的計(jì)算、搜索不經(jīng)常變化的數(shù)據(jù)或任何耗時(shí)的數(shù)學(xué)計(jì)算來(lái)說(shuō),記憶化這種方式是十分有用的。
看看下面的這個(gè)例子,它使用了一個(gè)簡(jiǎn)單的(也的確是效率不高的)算法來(lái)計(jì)算素?cái)?shù)。盡管這是一個(gè)復(fù)雜計(jì)算的簡(jiǎn)單例子,但它經(jīng)常被應(yīng)用到大計(jì)算量的場(chǎng)景中(例如可以引申到通過(guò)字符串生成MD5算法),這里不便展示。
從外表來(lái)說(shuō),這個(gè)函數(shù)和任何普通函數(shù)一樣,但在內(nèi)部我們會(huì)構(gòu)建一個(gè)結(jié)果緩存,它會(huì)保存函數(shù)每次計(jì)算得到的結(jié)果,如清單1.3所示。
清單1.3 計(jì)算先前得到的值
1function isPrime(value) {
2 if (!isPrime.answers) {
3 isPrime.answers = {};
4 } ?--- 創(chuàng)建緩存
5 if (isPrime.answers[value] !== undefined) {
6 return isPrime.answers[value];
7 } ?--- 檢查緩存的值
8 var prime = value !== 0 && value !== 1; // 1 is not a prime
9 for (var i = 2; i < value; i++) {
10 if (value % i === 0) {
11 prime = false;
12 break;
13 }
14 }
15 return isPrime.answers[value] = prime; ?--- 存儲(chǔ)計(jì)算的值
16}
17assert(isPrime(5), "5 is prime!");
18assert(isPrime.answers[5], "The answer was cached!"); ?--- 測(cè)試該函數(shù)是否正常工作
在isPrime函數(shù)中,首先通過(guò)檢查它的answers屬性來(lái)確認(rèn)是否已經(jīng)創(chuàng)建了一個(gè)緩存,如果沒(méi)有創(chuàng)建,則新建一個(gè):
1if (!isPrime.answers) {
2 isPrime.answers = {};
3}
只有第一次函數(shù)調(diào)用才會(huì)創(chuàng)建這個(gè)初始空對(duì)象,之后這個(gè)緩存就已經(jīng)存在了。然后我們會(huì)檢查參數(shù)中傳的值是否已經(jīng)存儲(chǔ)到緩存中:
1if (isPrime.answers[value] !== undefined) {
2 return isPrime.answers[value];
3}
這個(gè)緩存會(huì)針對(duì)參數(shù)中的值value來(lái)存儲(chǔ)該值是否為素?cái)?shù)(true或false)。如果我們?cè)诰彺嬷姓业皆撝担瘮?shù)會(huì)直接返回。
1return isPrime.answers[value] = prime;
這個(gè)緩存是函數(shù)自身的一個(gè)屬性,所以只要該函數(shù)還存在,緩存也就存在。
最后的測(cè)試結(jié)果可以看到記憶函數(shù)生效了。
1assert(isPrime(5), "5 is prime!");
2assert(isPrime.answers[5], "The answer was cached!");
這個(gè)方法具有兩個(gè)優(yōu)點(diǎn)。
由于函數(shù)調(diào)用時(shí)會(huì)尋找之前調(diào)用所得到的值,所以用戶(hù)最終會(huì)樂(lè)于看到所獲得的性能收益。
它幾乎是無(wú)縫地發(fā)生在后臺(tái),最終用戶(hù)和頁(yè)面作者都不需要執(zhí)行任何特殊請(qǐng)求,也不需要做任何額外初始化,就能順利進(jìn)行工作。
當(dāng)然這種方法并不是像玫瑰和提琴一樣完美,還是要權(quán)衡利弊。
任何類(lèi)型的緩存都必然會(huì)為性能犧牲內(nèi)存。
純粹主義者會(huì)認(rèn)為緩存邏輯不應(yīng)該和業(yè)務(wù)邏輯混合,函數(shù)或方法只需要把一件事做好。但不必?fù)?dān)心,在第8章你會(huì)了解到如何解決這類(lèi)問(wèn)題。
對(duì)于這類(lèi)問(wèn)題很難做負(fù)載測(cè)試或估計(jì)算法復(fù)雜度,因?yàn)榻Y(jié)果依賴(lài)于函數(shù)之前的輸入。
現(xiàn)在你看到了函數(shù)作為第一類(lèi)公民的一些實(shí)例,接下來(lái)看看不同的函數(shù)定義的方式。
1.3 函數(shù)定義
JavaScript函數(shù)通常由函數(shù)字面量(function literal)來(lái)創(chuàng)建函數(shù)值,就像數(shù)字字面量創(chuàng)建一個(gè)數(shù)字值一樣。要記住這一點(diǎn),作為第一類(lèi)對(duì)象,函數(shù)是可以用在編程語(yǔ)言中的值,就像例句字符串或數(shù)字的值。無(wú)論你是否意識(shí)到了這一點(diǎn),你一直都是這樣做的。
JavaScript提供了幾種定義函數(shù)的方式,可以分為4類(lèi)。
函數(shù)定義(function declarations)和函數(shù)表達(dá)式(function expressions)——最常用,在定義函數(shù)上卻有微妙不同的的兩種方式。人們通常不會(huì)獨(dú)立地看待它們,但正如你將看到的,意識(shí)到兩者的不同能幫我們理解函數(shù)何時(shí)能夠被調(diào)用。
1function myFun(){ return 1;}
箭頭函數(shù)(通常被叫做lambda函數(shù))——ES6新增的JavaScript標(biāo)準(zhǔn),能讓我們以盡量簡(jiǎn)潔的語(yǔ)法定義函數(shù)。
1myArg => myArg*2
函數(shù)構(gòu)造函數(shù)—— 一種不常使用的函數(shù)定義方式,能讓我們以字符串形式動(dòng)態(tài)構(gòu)造一個(gè)函數(shù),這樣得到的函數(shù)是動(dòng)態(tài)生成的。這個(gè)例子動(dòng)態(tài)地創(chuàng)建了一個(gè)函數(shù),其參數(shù)為a和b,返回值為兩個(gè)數(shù)的和。
1new Function('a', 'b', 'return a + b')
生成器函數(shù)——ES6新增功能,能讓我們創(chuàng)建不同于普通函數(shù)的函數(shù),在應(yīng)用程序執(zhí)行過(guò)程中,這種函數(shù)能夠退出再重新進(jìn)入,在這些再進(jìn)入之間保留函數(shù)內(nèi)變量的值。我們可以定義生成器版本的函數(shù)聲明、函數(shù)表達(dá)式、函數(shù)構(gòu)造函數(shù)。
1function* myGen(){ yield 1; }
理解這幾種方式的不同很重要,因?yàn)楹瘮?shù)創(chuàng)建的方式很大程度地影響了函數(shù)可被調(diào)用的時(shí)間、函數(shù)的行為以及函數(shù)可以在哪個(gè)對(duì)象上被調(diào)用。
這一節(jié)中,我們將會(huì)探索函數(shù)定義、函數(shù)表達(dá)式和箭頭函數(shù)。你將學(xué)到它們的語(yǔ)法和它們的工作方式,我們也將會(huì)在本文中多次回顧它們的細(xì)節(jié)。另一方面,生成器函數(shù)則有一點(diǎn)獨(dú)特,它不同于普通函數(shù)。在第6章我們會(huì)再來(lái)學(xué)習(xí)它們的細(xì)節(jié)。
剩下的JavaScript特性——函數(shù)構(gòu)造函數(shù)我們將全部跳過(guò)。盡管它具有某些有趣的應(yīng)用場(chǎng)景,尤其是在動(dòng)態(tài)創(chuàng)建和執(zhí)行代碼時(shí),但我們依然認(rèn)為它是JavaScript語(yǔ)言的邊緣功能。如果你想知道更多關(guān)于函數(shù)構(gòu)造函數(shù)的信息,請(qǐng)?jiān)L問(wèn)http://mng.bz/ZN8e。
讓我們先用最簡(jiǎn)單、最傳統(tǒng)的方式定義函數(shù)吧:函數(shù)聲明和函數(shù)表達(dá)式。
1.3.1 函數(shù)聲明和函數(shù)表達(dá)式
JavaScript中定義函數(shù)最常用的方式是函數(shù)聲明和函數(shù)表達(dá)式。這兩種技術(shù)非常相似,有時(shí)甚至難以區(qū)分,但在后續(xù)章節(jié)中你將看到,它們之間還是存在著微妙的差別。
函數(shù)聲明
JavaScript定義函數(shù)的最基本方式是函數(shù)聲明(見(jiàn)圖1.4)。正如你所見(jiàn),每個(gè)函數(shù)聲明以強(qiáng)制性的function開(kāi)頭,其后緊接著強(qiáng)制性的函數(shù)名,以及括號(hào)和括號(hào)內(nèi)一列以逗號(hào)分隔的可選參數(shù)名。函數(shù)體是一列可以為空的表達(dá)式,這些表達(dá)式必須包含在花括號(hào)內(nèi)。除了這種形式以外,每個(gè)函數(shù)聲明還必須包含一個(gè)條件:作為一個(gè)單獨(dú)的JavaScript語(yǔ)句,函數(shù)聲明必須獨(dú)立(但也能夠被包含在其他函數(shù)或代碼塊中,在下一小節(jié)中你將會(huì)準(zhǔn)確理解其含義)。
圖1.4 函數(shù)聲明是獨(dú)立的,是獨(dú)立的JavaScript代碼塊(它可以被包含在其他函數(shù)中)
清單1.4展示了兩條函數(shù)聲明例子。
清單1.4 函數(shù)聲明示例
1function samurai() {
2 return "samurai here"; ?--- 在全局代碼中定義samurai函數(shù)
3}
4function ninja() { ?--- 在全局代碼中定義ninja函數(shù)
5 function hiddenNinja() {
6 return "ninja here";
7 } ?--- 在ninja函數(shù)內(nèi)定義hiddenNinja函數(shù)
8 return hiddenNinja();
9}
如果你對(duì)函數(shù)式語(yǔ)言沒(méi)有太多了解,仔細(xì)看一看,你可能會(huì)發(fā)現(xiàn)你并不習(xí)慣這種使用方式: 一個(gè)函數(shù)被定義在另一個(gè)函數(shù)之中!
1function ninja() {
2 function hiddenNinja() {
3 return "ninja here";
4 }
5 return hiddenNinja();
6}
在JavaScript中,這是一種非常通用的使用方式,這里用它作為例子是為了再次強(qiáng)調(diào)JavaScript中函數(shù)的重要性。
{注意 }
讓函數(shù)包含在另一個(gè)函數(shù)中可能會(huì)因?yàn)楹雎宰饔糜虻臉?biāo)識(shí)符解析而引發(fā)一些有趣的問(wèn)題,但現(xiàn)在可以先留下這個(gè)問(wèn)題,第5章會(huì)重新回顧這個(gè)問(wèn)題的細(xì)節(jié)。
函數(shù)表達(dá)式
正如我們多次所提到的,JavaScript中的函數(shù)是第一類(lèi)對(duì)象,除此以外也就意味著它們可以通過(guò)字面量創(chuàng)建,可以賦值給變量和屬性,可以作為傳遞給其他函數(shù)的參數(shù)或函數(shù)的返回值。正因?yàn)楹瘮?shù)有如此的基礎(chǔ)結(jié)構(gòu),所以JavaScript能讓我們把函數(shù)和其他表達(dá)式同等看待。例如,如下例子中我們可以使用數(shù)字字面量:
1var a = 3;
2myFunction(4);
同樣,在相同位置可以用函數(shù)字面量:
1var a = function() {};
2myFunction(function(){});
這種總是其他表達(dá)式的一部分的函數(shù)(作為賦值表達(dá)式的右值,或者作為其他函數(shù)的參數(shù))叫作函數(shù)表達(dá)式。函數(shù)表達(dá)式非常重要,在于它能準(zhǔn)確地在我們需要使用的地方定義函數(shù),這個(gè)過(guò)程能讓代碼易于理解。清單1.5展示了函數(shù)聲明和函數(shù)表達(dá)式的不同之處。
清單1.5 函數(shù)聲明和函數(shù)表達(dá)式
1<pre class="代碼無(wú)行號(hào)"><code>function myFunctionDeclaration(){ ?--- 獨(dú)立的函數(shù)聲明
2 function innerFunction() {} ?--- 內(nèi)部函數(shù)聲明
3}
4var myFunc = function(){}; ?--- 函數(shù)表達(dá)式作為變量聲明賦值語(yǔ)句中的一部分
5myFunc(function(){ ?--- 函數(shù)表達(dá)式作為一次函數(shù)調(diào)用中的參數(shù)
6 return function(){}; ?--- 函數(shù)表達(dá)式作為函數(shù)返回值
7});
8(function <strong>namedFunctionExpression</strong> () {
9})(); ?--- 作為函數(shù)調(diào)用的一部分,命名函數(shù)表達(dá)式會(huì)被立即調(diào)用
10+function(){}();
11-function(){}();
12!function(){}();
13~function(){}(); ?--- 函數(shù)有達(dá)式可以作為一元操作符的參數(shù)立即調(diào)用</code></pre>
示例代碼的開(kāi)頭是標(biāo)準(zhǔn)函數(shù)聲明,其包含一個(gè)內(nèi)部函數(shù)聲明:
1function myFunctionDeclaration(){
2 function innerFunction() {}
3}
從這個(gè)示例中你能夠看到,函數(shù)聲明是如何作為JavaScript代碼中的獨(dú)立表達(dá)式的,但它也能夠包含在其他函數(shù)體內(nèi)。與之比較的是函數(shù)表達(dá)式,它通常作為其他語(yǔ)句的一部分。它們被放在表達(dá)式級(jí)別,作為變量聲明(或者賦值)的右值:
1var myFunc = function(){};
或者作為另一個(gè)函數(shù)調(diào)用的參數(shù)或返回值。
1myFunc(function() {
2 return function(){};
3});
函數(shù)聲明和函數(shù)表達(dá)式除了在代碼中的位置不同以外,還有一個(gè)更重要的不同點(diǎn)是:對(duì)于函數(shù)聲明來(lái)說(shuō),函數(shù)名是強(qiáng)制性的,而對(duì)于函數(shù)表達(dá)式來(lái)說(shuō),函數(shù)名則完全是可選的。
函數(shù)聲明必須具有函數(shù)名是因?yàn)樗鼈兪仟?dú)立語(yǔ)句。一個(gè)函數(shù)的基本要求是它應(yīng)該能夠被調(diào)用,所以它必須具有一種被引用方式,于是唯一的方式就是通過(guò)它的名字。
從另一方面來(lái)看,函數(shù)表達(dá)式也是其他JavaScript表達(dá)式的一部分,所以我們也就具有了調(diào)用它們的替代方案。例如,如果一個(gè)函數(shù)表達(dá)式被賦值給了一個(gè)變量,我們可以用該變量來(lái)調(diào)用函數(shù)。
1var doNothing = function(){};
2doNothing();
或者,如果它是另外一個(gè)函數(shù)的參數(shù),我們可以在該函數(shù)中通過(guò)相應(yīng)的參數(shù)名來(lái)調(diào)用它。
1function doSomething(action) {
2 action();
3}
立即函數(shù)
函數(shù)表達(dá)式可以放在初看起來(lái)有些奇怪的位置上,例如通常認(rèn)為是函數(shù)標(biāo)識(shí)符的位置。接下來(lái)仔細(xì)看看這個(gè)構(gòu)造(如圖1.5所示)。
圖1.5 標(biāo)準(zhǔn)函數(shù)的調(diào)用和函數(shù)表達(dá)式的立即調(diào)用的對(duì)比
當(dāng)想進(jìn)行函數(shù)調(diào)用時(shí),我們需要使用能夠求值得到函數(shù)的表達(dá)式,其后跟著一對(duì)函數(shù)調(diào)用括號(hào),括號(hào)內(nèi)包含參數(shù)。在最基本的函數(shù)調(diào)用中,我們把求值得到函數(shù)的標(biāo)識(shí)符作為左值(如圖1.5所示)。不過(guò)用于被括號(hào)調(diào)用的表達(dá)式不必只是一個(gè)簡(jiǎn)單的標(biāo)識(shí)符,它可以是任何能夠求值得到函數(shù)的表達(dá)式。例如,指定一個(gè)求值得到函數(shù)的表達(dá)式的最簡(jiǎn)單方式是使用函數(shù)表達(dá)式。如圖1.5中右圖所示,我們首先創(chuàng)建了一個(gè)函數(shù),然后立即調(diào)用這個(gè)新創(chuàng)建的函數(shù)。這種函數(shù)叫作立即調(diào)用函數(shù)表達(dá)式(IIFE),或者簡(jiǎn)寫(xiě)為立即函數(shù)。這一特性能夠模擬JavaScript中的模塊化,故可以說(shuō)它是JavaScript開(kāi)發(fā)中的重要理念。第11章中會(huì)集中討論IIFE的應(yīng)用。
{加括號(hào)的函數(shù)表達(dá)式!}
還有一件可能困擾你的是上面例子中我們立即調(diào)用的函數(shù)表達(dá)式方式:函數(shù)表達(dá)式被包裹在一對(duì)括號(hào)內(nèi)。為什么這樣做呢?其原因是純語(yǔ)法層面的。JavaScript解析器必須能夠輕易區(qū)分函數(shù)聲明和函數(shù)表達(dá)式之間的區(qū)別。如果去掉包裹函數(shù)表達(dá)式的括號(hào),把立即調(diào)用作為一個(gè)獨(dú)立語(yǔ)句function() {}(3),JavaScript開(kāi)始解析時(shí)便會(huì)結(jié)束,因?yàn)檫@個(gè)獨(dú)立語(yǔ)句以function開(kāi)頭,那么解析器就會(huì)認(rèn)為它在處理一個(gè)函數(shù)聲明。每個(gè)函數(shù)聲明必須有一個(gè)名字(然而這里并沒(méi)有指定名字),所以程序執(zhí)行到這里會(huì)報(bào)錯(cuò)。為了避免錯(cuò)誤,函數(shù)表達(dá)式要放在括號(hào)內(nèi),為JavaScript解析器指明它正在處理一個(gè)函數(shù)表達(dá)式而不是語(yǔ)句。
還有一種相對(duì)簡(jiǎn)單的替代方案(function(){}(3))也能達(dá)到相同目標(biāo)(然而這種方案有些奇怪,故不常使用)。把立即函數(shù)的定義和調(diào)用都放在括號(hào)內(nèi),同樣可以為JavaScript解析器指明它正在處理函數(shù)表達(dá)式。
表1.5中最后4個(gè)表達(dá)式都是立即調(diào)用函數(shù)表達(dá)式主題的4個(gè)不同版本,在JavaScript庫(kù)中會(huì)經(jīng)常見(jiàn)到這幾種形式:
1+function(){}();
2-function(){}();
3!function(){}();
4~function(){}();
不同于用加括號(hào)的方式區(qū)分函數(shù)表達(dá)式和函數(shù)聲明,這里我們使用一元操作符+、-、!和~。這種做法也是用于向JavaScript引擎指明它處理的是表達(dá)式,而不是語(yǔ)句。從計(jì)算機(jī)的角度來(lái)講,注意應(yīng)用一元操作符得到的結(jié)果沒(méi)有存儲(chǔ)到任何地方并不重要,只有調(diào)用IIFE才重要。現(xiàn)在我們已經(jīng)學(xué)會(huì)了JavaScript中兩種基本的函數(shù)定義方式(函數(shù)聲明和函數(shù)表達(dá)式)的細(xì)節(jié)。接下來(lái)開(kāi)始探索JavaScript標(biāo)準(zhǔn)中的新增特性:箭頭函數(shù)。
1.3.2 箭頭函數(shù)
注意:
箭頭函數(shù)是JavaScript標(biāo)準(zhǔn)中的ES6新增項(xiàng)(瀏覽器兼容性可參考http://mng.bz/8bnH)。
由于JavaScript中會(huì)使用大量函數(shù),增加簡(jiǎn)化創(chuàng)建函數(shù)方式的語(yǔ)法十分有意義,也能讓我們的開(kāi)發(fā)者生活更愉快。在很多方式中,箭頭函數(shù)是函數(shù)表達(dá)式的簡(jiǎn)化版。一起來(lái)回顧一下本文開(kāi)始的排序例子。
1var values = [0, 3, 2, 5, 7, 4, 8, 1];
2values.sort(function(value1,value2){
3 return value1 – value2;
4});
這個(gè)例子中,數(shù)組對(duì)象的排序方法的參數(shù)傳入了一個(gè)回調(diào)函數(shù)表達(dá)式,JavaScript引擎會(huì)調(diào)用這個(gè)回調(diào)函數(shù)以降序排序數(shù)組。現(xiàn)在來(lái)看看如何用箭頭函數(shù)來(lái)做完全相同的工作:
1var values = [0, 3, 2, 5, 7, 4, 8, 1];
2
3values.sort((
4
5value1,value2) => value1 – value2
6
7);
看到這是多么簡(jiǎn)潔了吧?
這種寫(xiě)法不會(huì)產(chǎn)生任何因?yàn)闀?shū)寫(xiě)function關(guān)鍵字、大括號(hào)或者return語(yǔ)句導(dǎo)致的混亂。箭頭函數(shù)語(yǔ)句有著比函數(shù)表達(dá)式更為簡(jiǎn)單的方式:函數(shù)傳入兩個(gè)參數(shù)并返回其差值。注意這個(gè)新操作符——胖箭頭符號(hào)=>(等號(hào)后面跟著大于號(hào))是定義箭頭函數(shù)的核心。
現(xiàn)在來(lái)解析箭頭函數(shù)的語(yǔ)法,首先看看它的最簡(jiǎn)形式:
1param => expression
這個(gè)箭頭函數(shù)接收一個(gè)參數(shù)并返回表達(dá)式的值,如下面的清單1.6就使用了這種語(yǔ)法。
清單1.6 比較箭頭函數(shù)和函數(shù)表達(dá)式
1var greet = name => "Greetings " + name; ?--- 定義箭頭函數(shù)
2
3assert(greet("Oishi") === "Greetings Oishi", "Oishi is properly greeted")
4;
5
6var anotherGreet = function(nam
7e){
8 return "Greetings " + n
9ame;
10}; ?--- 定義
11函數(shù)表達(dá)式
12assert(anotherGreet("Oishi") === "Greetings O
13ishi",
14 "Again, Oishi is properly greeted");
稍作欣賞,使用箭頭函數(shù)的代碼即簡(jiǎn)潔又清楚。這是箭頭函數(shù)的最簡(jiǎn)語(yǔ)法,但一般情況下,箭頭函數(shù)會(huì)被定義成兩種方式,如圖1.6所示。
稍作欣賞,使用箭頭函數(shù)的代碼即簡(jiǎn)潔又清楚。這是箭頭函數(shù)的最簡(jiǎn)語(yǔ)法,但一般情況下,箭頭函數(shù)會(huì)被定義成兩種方式,如圖1.6所示。
圖1.6 箭頭函數(shù)的語(yǔ)法
如你所見(jiàn),箭頭函數(shù)的定義以一串可選參數(shù)名列表開(kāi)頭,參數(shù)名以逗號(hào)分隔。如果沒(méi)有參數(shù)或者多余一個(gè)參數(shù)時(shí),參數(shù)列表就必須包裹在括號(hào)內(nèi)。但如果只有一個(gè)參數(shù)時(shí),括號(hào)就不是必須的。參數(shù)列表之后必須跟著一個(gè)胖箭頭符號(hào),以此向我們和JavaScript引擎指示當(dāng)前處理的是箭頭函數(shù)。
胖箭頭操作符后面有兩種可選方式。如果要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單函數(shù),那么可以把表達(dá)式放在這里(可以是數(shù)學(xué)運(yùn)算、其他的函數(shù)調(diào)用等),則該函數(shù)的返回值即為此表達(dá)式的返回值。例如,第一個(gè)箭頭函數(shù)的示例如下:
1var greet = name => "Greetings " + name;
這個(gè)箭頭函數(shù)的返回值是字符串“Greetings”和參數(shù)name的結(jié)合。在其他案例中,當(dāng)箭頭函數(shù)沒(méi)那么簡(jiǎn)單從而需要更多代碼時(shí),箭頭操作符后則可以跟一個(gè)代碼塊,例如:
1var greet = name => {
2 var helloString = 'Greetings ';
3 return helloString + name;
4};
這段代碼中箭頭函數(shù)的返回值和普通函數(shù)一樣。如果沒(méi)有return語(yǔ)句,返回值是undefined;反之,返回值就是return表達(dá)式的值。
在本文中我們會(huì)多次回顧箭頭函數(shù)。除此之外,我們還會(huì)展示箭頭函數(shù)的一些額外功能,它能幫助我們規(guī)避一些在很多標(biāo)準(zhǔn)函數(shù)中可能遇到的難以捉摸的缺陷。箭頭函數(shù)和很多其他函數(shù)一樣,可以通過(guò)接收參數(shù)來(lái)執(zhí)行任務(wù)。接下來(lái)看看當(dāng)向函數(shù)內(nèi)傳入?yún)?shù)后,該參數(shù)值發(fā)生了什么。
本文摘自《JavaScript忍者秘籍(第2版)》
《JavaScript忍者秘籍 第2版》
[美] John,Resig(萊西格),Bear,Bibeault(貝比奧特),Josip ... 著
點(diǎn)擊封面購(gòu)買(mǎi)紙書(shū)
JavaScript 正以驚人的速度成為各種應(yīng)用程序的通用語(yǔ)言,包括 Web、桌面、云和移動(dòng)設(shè)備上的應(yīng)用程序。當(dāng)成為 JavaScript 專(zhuān)業(yè)開(kāi)發(fā)者時(shí),你將擁有可應(yīng)用于所有這些領(lǐng)域的、強(qiáng)大的技能集。
《JavaScript 忍者秘籍(第2版)》使用實(shí)際的案例清晰地詮釋每一個(gè)核心概念和技術(shù)。本書(shū)向讀者介紹了如何掌握 JavaScript 核心的概念,諸如函數(shù)、閉包、對(duì)象、原型和 promise,同時(shí)還介紹了 JavaScript API, 包括 DOM、事件和計(jì)時(shí)器。你將學(xué)會(huì)測(cè)試、跨瀏覽器開(kāi)發(fā),所有這些都是高級(jí)JavaScript開(kāi)發(fā)者應(yīng)該掌握的技能。
小福利
關(guān)注【異步社區(qū)】服務(wù)號(hào),轉(zhuǎn)發(fā)本文至朋友圈或 50 人以上微信群,截圖發(fā)送至異步社區(qū)服務(wù)號(hào)后臺(tái),并在文章底下留言,分享你的JavaScript開(kāi)發(fā)經(jīng)驗(yàn)或者本書(shū)的試讀體驗(yàn),我們將選出3名讀者贈(zèng)送《JavaScript 忍者秘籍(第2版)》1本,趕快積極參與吧!
活動(dòng)截止時(shí)間:2018 年3月15日
在“異步社區(qū)”后臺(tái)回復(fù)“關(guān)注”,即可免費(fèi)獲得2000門(mén)在線視頻課程;推薦朋友關(guān)注根據(jù)提示獲取贈(zèng)書(shū)鏈接,免費(fèi)得異步圖書(shū)一本。趕緊來(lái)參加哦!
掃一掃上方二維碼,回復(fù)“關(guān)注”參與活動(dòng)!
閱讀原文
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。