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