[TOC]
2017年第一波 JavaScript 面試題
一道常被人輕視的前端****JS面試題
目錄
- 前言
- 第一問
- 第二問
- 變量聲明提升
- 函數(shù)表達(dá)式
- 第三問
- 第四問
- 第五問
- 第六問
- 構(gòu)造函數(shù)的返回值
- 第七問
- 最后
前言
年前剛剛離職了盯另,分享下我曾經(jīng)出過的一道面試題,此題是我出的一套前端面試題中的最后一題柄粹,用來考核面試者的JavaScript的綜合能力酷窥,很可惜到目前為止的將近兩年中昵宇,幾乎沒有人能夠完全答對(duì),并非多難只是因?yàn)榇蠖嗝嬖囌哌^于輕視他。
題目如下:
function Foo() {
getName = function () { alert (1); };
return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}
//答案:
Foo.getName();//2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3
此題是我綜合之前的開發(fā)經(jīng)驗(yàn)以及遇到的JS各種坑匯集而成雹食。此題涉及的知識(shí)點(diǎn)眾多,包括變量定義提升期丰、this指針指向群叶、運(yùn)算符優(yōu)先級(jí)、原型钝荡、繼承街立、全局變量污染、對(duì)象屬性及原型屬性優(yōu)先級(jí)等等埠通。
此題包含7小問赎离,分別說下。
第一問
先看此題的上半部分做了什么端辱,首先定義了一個(gè)叫Foo的函數(shù)蟹瘾,之后為Foo創(chuàng)建了一個(gè)叫g(shù)etName的靜態(tài)屬性存儲(chǔ)了一個(gè)匿名函數(shù),之后為Foo的原型對(duì)象新創(chuàng)建了一個(gè)叫g(shù)etName的匿名函數(shù)掠手。之后又通過函數(shù)變量表達(dá)式創(chuàng)建了一個(gè)getName的函數(shù)憾朴,最后再聲明一個(gè)叫g(shù)etName函數(shù)。
第一問的 Foo.getName 自然是訪問Foo函數(shù)上存儲(chǔ)的靜態(tài)屬性喷鸽,自然是2众雷,沒什么可說的。
第二問
第二問,直接調(diào)用 getName 函數(shù)砾省。既然是直接調(diào)用那么就是訪問當(dāng)前上文作用域內(nèi)的叫g(shù)etName的函數(shù)鸡岗,所以跟1 2 3都沒什么關(guān)系。此題有無數(shù)面試者回答為5编兄。此處有兩個(gè)坑轩性,一是變量聲明提升,二是函數(shù)表達(dá)式狠鸳。
變量聲明提升
即所有聲明變量或聲明函數(shù)都會(huì)被提升到當(dāng)前函數(shù)的頂部揣苏。
例如下代碼:
console.log('x' in window);//true
var x;
x = 0;
代碼執(zhí)行時(shí)js引擎會(huì)將聲明語(yǔ)句提升至代碼最上方,變?yōu)椋?/p>
var x;
console.log('x' in window);//true
x = 0;
函數(shù)表達(dá)式
var getName 與 function getName 都是聲明語(yǔ)句件舵,區(qū)別在于 var getName 是函數(shù)表達(dá)式卸察,而 function getName 是函數(shù)聲明。關(guān)于JS中的各種函數(shù)創(chuàng)建方式可以看 大部分人都會(huì)做錯(cuò)的經(jīng)典JS閉包面試題 這篇文章有詳細(xì)說明铅祸。
函數(shù)表達(dá)式最大的問題坑质,在于js會(huì)將此代碼拆分為兩行代碼分別執(zhí)行。
例如下代碼:
console.log(x);//輸出:function x(){}
var x=1;
function x(){}
實(shí)際執(zhí)行的代碼為临梗,先將 var x=1 拆分為 var x; 和 x = 1; 兩行涡扼,再將 var x; 和 function x(){} 兩行提升至最上方變成:
var x;
function x(){}
console.log(x);
x=1;
所以最終函數(shù)聲明的x覆蓋了變量聲明的x,log輸出為x函數(shù)盟庞。
同理吃沪,原題中代碼最終執(zhí)行時(shí)的是:
function Foo() {
getName = function () { alert (1); };
return this;
}
var getName;//只提升變量聲明
function getName() { alert (5);}//提升函數(shù)聲明,覆蓋var的聲明
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
getName = function () { alert (4);};//最終的賦值再次覆蓋function getName聲明
getName();//最終輸出4
第三問
第三問的 Foo().getName(); 先執(zhí)行了Foo函數(shù)茫经,然后調(diào)用Foo函數(shù)的返回值對(duì)象的getName屬性函數(shù)巷波。
Foo函數(shù)的第一句 getName = function () { alert (1); }; 是一句函數(shù)賦值語(yǔ)句,注意它沒有var聲明卸伞,所以先向當(dāng)前Foo函數(shù)作用域內(nèi)尋找getName變量抹镊,沒有。再向當(dāng)前函數(shù)作用域上層荤傲,即外層作用域內(nèi)尋找是否含有g(shù)etName變量垮耳,找到了,也就是第二問中的alert(4)函數(shù)遂黍,將此變量的值賦值為 function(){alert(1)}终佛。
此處實(shí)際上是將外層作用域內(nèi)的getName函數(shù)修改了。
注意:此處若依然沒有找到會(huì)一直向上查找到window對(duì)象雾家,若window對(duì)象中也沒有g(shù)etName屬性铃彰,就在window對(duì)象中創(chuàng)建一個(gè)getName變量。
之后Foo函數(shù)的返回值是this芯咧,而JS的this問題博客園中已經(jīng)有非常多的文章介紹牙捉,這里不再多說竹揍。
簡(jiǎn)單的講,this的指向是由所在函數(shù)的調(diào)用方式?jīng)Q定的邪铲。而此處的直接調(diào)用方式芬位,this指向window對(duì)象。
遂Foo函數(shù)返回的是window對(duì)象带到,相當(dāng)于執(zhí)行 window.getName() 昧碉,而window中的getName已經(jīng)被修改為alert(1),所以最終會(huì)輸出1
此處考察了兩個(gè)知識(shí)點(diǎn)揽惹,一個(gè)是變量作用域問題被饿,一個(gè)是this指向問題。
第四問
直接調(diào)用getName函數(shù)永丝,相當(dāng)于 window.getName() 锹漱,因?yàn)檫@個(gè)變量已經(jīng)被Foo函數(shù)執(zhí)行時(shí)修改了箭养,遂結(jié)果與第三問相同慕嚷,為1
第五問
第五問 new Foo.getName(); ,此處考察的是js的運(yùn)算符優(yōu)先級(jí)問題。
js運(yùn)算符優(yōu)先級(jí):
通過查上表可以得知點(diǎn)(.)的優(yōu)先級(jí)高于new操作毕泌,遂相當(dāng)于是:
new (Foo.getName)();
所以實(shí)際上將getName函數(shù)作為了構(gòu)造函數(shù)來執(zhí)行喝检,遂彈出2。
第六問
第六問 new Foo().getName() 撼泛,首先看運(yùn)算符優(yōu)先級(jí)括號(hào)高于new挠说,實(shí)際執(zhí)行為
(new Foo()).getName()
遂先執(zhí)行Foo函數(shù),而Foo此時(shí)作為構(gòu)造函數(shù)卻有返回值愿题,所以這里需要說明下js中的構(gòu)造函數(shù)返回值問題损俭。
構(gòu)造函數(shù)的返回值
在傳統(tǒng)語(yǔ)言中,構(gòu)造函數(shù)不應(yīng)該有返回值潘酗,實(shí)際執(zhí)行的返回值就是此構(gòu)造函數(shù)的實(shí)例化對(duì)象杆兵。
而在js中構(gòu)造函數(shù)可以有返回值也可以沒有。
1仔夺、沒有返回值則按照其他語(yǔ)言一樣返回實(shí)例化對(duì)象琐脏。
2、若有返回值則檢查其返回值是否為引用類型缸兔。如果是非引用類型日裙,如基本類型(string,number,boolean,null,undefined)則與無返回值相同,實(shí)際返回其實(shí)例化對(duì)象惰蜜。
3昂拂、若返回值是引用類型,則實(shí)際返回值為這個(gè)引用類型抛猖。
原題中格侯,返回的是this路克,而this在構(gòu)造函數(shù)中本來就代表當(dāng)前實(shí)例化對(duì)象,遂最終Foo函數(shù)返回實(shí)例化對(duì)象养交。
之后調(diào)用實(shí)例化對(duì)象的getName函數(shù)精算,因?yàn)樵贔oo構(gòu)造函數(shù)中沒有為實(shí)例化對(duì)象添加任何屬性,遂到當(dāng)前對(duì)象的原型對(duì)象(prototype)中尋找getName碎连,找到了灰羽。
遂最終輸出3。
第七問
第七問, new new Foo().getName(); 同樣是運(yùn)算符優(yōu)先級(jí)問題鱼辙。
最終實(shí)際執(zhí)行為:
new ((new Foo()).getName)();
先初始化Foo的實(shí)例化對(duì)象廉嚼,然后將其原型上的getName函數(shù)作為構(gòu)造函數(shù)再次new。
遂最終結(jié)果為3
===2016年03月23日更新===
這里引用 @于明昊 的評(píng)論倒戏,更詳細(xì)的解釋了第7問:
這里確實(shí)是(new Foo()).getName()怠噪,但是跟括號(hào)優(yōu)先級(jí)高于成員訪問沒關(guān)系,實(shí)際上這里成員訪問的優(yōu)先級(jí)是最高的杜跷,因此先執(zhí)行了 .getName傍念,但是在進(jìn)行左側(cè)取值的時(shí)候, new Foo() 可以理解為兩種運(yùn)算:new 帶參數(shù)(即 new Foo())和函數(shù)調(diào)用(即 先 Foo() 取值之后再 new)葛闷,而 new 帶參數(shù)的優(yōu)先級(jí)是高于函數(shù)調(diào)用的憋槐,因此先執(zhí)行了 new Foo(),或得 Foo 類的實(shí)例對(duì)象淑趾,再進(jìn)行了成員訪問 .getName阳仔。
最后
就答題情況而言,第一問100%都可以回答正確扣泊,第二問大概只有50%正確率近范,第三問能回答正確的就不多了,第四問再正確就非常非常少了延蟹。其實(shí)此題并沒有太多刁鉆匪夷所思的用法评矩,都是一些可能會(huì)遇到的場(chǎng)景,而大多數(shù)人但凡有1年到2年的工作經(jīng)驗(yàn)都應(yīng)該完全正確才對(duì)等孵。
只能說有一些人太急躁太輕視了稚照,希望大家通過此文了解js一些特性。
并祝愿大家在新的一年找工作面試中膽大心細(xì)俯萌,發(fā)揮出最好的水平果录,找到一份理想的工作。
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
這段代碼很短咐熙,只有 7 行弱恒,我想,能讀到這里的同學(xué)應(yīng)該不需要我逐行解釋這段代碼在做什么吧棋恼。候選人面對(duì)這段代碼時(shí)給出的結(jié)果也不盡相同返弹,以下是典型的答案:
- A. 20% 的人會(huì)快速掃描代碼锈玉,然后給出結(jié)果:0,1,2,3,4,5;
- B. 30% 的人會(huì)拿著代碼逐行看义起,然后給出結(jié)果:5,0,1,2,3,4拉背;
- C. 50% 的人會(huì)拿著代碼仔細(xì)琢磨,然后給出結(jié)果:5,5,5,5,5,5默终;
只要你對(duì) JS 中同步和異步代碼的區(qū)別椅棺、變量作用域、閉包等概念有正確的理解齐蔽,就知道正確答案是 C两疚,代碼的實(shí)際輸出是:
2017-03-18T00:43:45.873Z 5
2017-03-18T00:43:46.866Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
接下來我會(huì)追問:如果我們約定,用箭頭表示其前后的兩次輸出之間有 1 秒的時(shí)間間隔含滴,而逗號(hào)表示其前后的兩次輸出之間的時(shí)間間隔可以忽略诱渤,代碼實(shí)際運(yùn)行的結(jié)果該如何描述?會(huì)有下面兩種答案:
- A. 60% 的人會(huì)描述為:5 -> 5 -> 5 -> 5 -> 5谈况,即每個(gè) 5 之間都有 1 秒的時(shí)間間隔勺美;
- B. 40% 的人會(huì)描述為:5 -> 5,5,5,5,5,即第 1 個(gè) 5 直接輸出鸦做,1 秒之后励烦,輸出 5 個(gè) 5谓着;
這就要求候選人對(duì) JS 中的定時(shí)器工作機(jī)制非常熟悉泼诱,循環(huán)執(zhí)行過程中,幾乎同時(shí)設(shè)置了 5 個(gè)定時(shí)器赊锚,一般情況下治筒,這些定時(shí)器都會(huì)在 1 秒之后觸發(fā),而循環(huán)完的輸出是立即執(zhí)行的舷蒲,顯而易見耸袜,正確的描述是 B。
如果到這里算是及格的話牲平,100 個(gè)人參加面試只有 20 人能及格堤框,讀到這里的同學(xué)可以仔細(xì)思考,你及格了么纵柿?
> 追問 1:閉包
如果這道題僅僅是考察候選人對(duì) JS 異步代碼蜈抓、變量作用域的理解,局限性未免太大昂儒,接下來我會(huì)追問沟使,如果期望代碼的輸出變成:5 -> 0,1,2,3,4,該怎么改造代碼渊跋?熟悉閉包的同學(xué)很快能給出下面的解決辦法:
for (var i = 0; i < 5; i++) {
(function(j) { // j = i
setTimeout(function() {
console.log(new Date, j);
}, 1000);
})(i);
}
console.log(new Date, i);
巧妙的利用 IIFE(Immediately Invoked Function Expression:聲明即執(zhí)行的函數(shù)表達(dá)式)來解決閉包造成的問題腊嗡,確實(shí)是不錯(cuò)的思路着倾,但是初學(xué)者可能并不覺得這樣的代碼很好懂,至少筆者初入門的時(shí)候這里琢磨了一會(huì)兒才真正理解燕少。
有沒有更符合直覺的做法卡者?答案是有,我們只需要對(duì)循環(huán)體稍做手腳客们,讓負(fù)責(zé)輸出的那段代碼能拿到每次循環(huán)的 i 值即可虎眨。該怎么做呢?利用 JS 中基本類型(Primitive Type)的參數(shù)傳遞是按值傳遞(Pass by Value)的特征镶摘,不難改造出下面的代碼:
var output = function (i) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i); // 這里傳過去的 i 值被復(fù)制了
}
console.log(new Date, i);
能給出上述 2 種解決方案的候選人可以認(rèn)為對(duì) JS 基礎(chǔ)的理解和運(yùn)用是不錯(cuò)的嗽桩,可以各加 10 分。當(dāng)然實(shí)際面試中還有候選人給出如下的代碼:
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)凄敢,這里只有個(gè)非常細(xì)微的變動(dòng)碌冶,即使用 ES6 塊級(jí)作用域(Block Scope)中的 let 替代了 var,但是代碼在實(shí)際運(yùn)行時(shí)會(huì)報(bào)錯(cuò)涝缝,因?yàn)樽詈竽莻€(gè)輸出使用的 i 在其所在的作用域中并不存在扑庞,i 只存在于循環(huán)內(nèi)部。
能想到 ES6 特性的同學(xué)雖然沒有答對(duì)拒逮,但是展示了自己對(duì) ES6 的了解罐氨,可以加 5 分,繼續(xù)進(jìn)行下面的追問滩援。
追問 2:ES6
有經(jīng)驗(yàn)的前端同學(xué)讀到這里可能有些不耐煩了栅隐,扯了這么多,都是他知道的內(nèi)容玩徊,先別著急租悄,挑戰(zhàn)的難度會(huì)繼續(xù)增加。
接著上文繼續(xù)追問:如果期望代碼的輸出變成 0 -> 1 -> 2 -> 3 -> 4 -> 5恩袱,并且要求原有的代碼塊中的循環(huán)和兩處 console.log 不變泣棋,該怎么改造代碼?新的需求可以精確的描述為:代碼執(zhí)行時(shí)畔塔,立即輸出 0潭辈,之后每隔 1 秒依次輸出 1,2,3,4,循環(huán)結(jié)束后在大概第 5 秒的時(shí)候輸出 5(這里使用大概澈吨,是為了避免鉆牛角尖的同學(xué)陷進(jìn)去把敢,因?yàn)?JS 中的定時(shí)器觸發(fā)時(shí)機(jī)有可能是不確定的,具體可參見 How Javascript Timers Work)棚辽。
看到這里技竟,部分同學(xué)會(huì)給出下面的可行解:
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(new Date, j);
}, 1000 * j)); // 這里修改 0~4 的定時(shí)器時(shí)間
})(i);
}
setTimeout(function() { // 這里增加定時(shí)器,超時(shí)設(shè)置為 5 秒
console.log(new Date, i);
}, 1000 * i);
不得不承認(rèn)屈藐,這種做法雖粗暴有效榔组,但是不算是能額外加分的方案熙尉。如果把這次的需求抽象為:在系列異步操作完成(每次循環(huán)都產(chǎn)生了 1 個(gè)異步操作)之后,再做其他的事情搓扯,代碼該怎么組織检痰?聰明的你是不是想起了什么?對(duì)锨推,就是 Promise铅歼。
可能有的同學(xué)會(huì)問,不就是在控制臺(tái)輸出幾個(gè)數(shù)字么换可?至于這樣殺雞用牛刀椎椰?你要知道,面試官真正想考察的是候選人是否具備某種能力和素質(zhì)沾鳄,因?yàn)樵诂F(xiàn)代的前端開發(fā)中慨飘,處理異步的代碼隨處可見,熟悉和掌握異步操作的流程控制是成為合格開發(fā)者的基本功译荞。
順著下來瓤的,不難給出基于 Promise 的解決方案(既然 Promise 是 ES6 中的新特性,我們的新代碼使用 ES6 編寫是不是會(huì)更好吞歼?如果你這么寫了圈膏,大概率會(huì)讓面試官心生好感):
const tasks = [];
for (var i = 0; i < 5; i++) { // 這里 i 的聲明不能改成 let,如果要改該怎么做篙骡?
((j) => {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, j);
resolve(); // 這里一定要 resolve稽坤,否則代碼不會(huì)按預(yù)期 work
}, 1000 * j); // 定時(shí)器的超時(shí)時(shí)間逐步增加
}));
})(i);
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000); // 注意這里只需要把超時(shí)設(shè)置為 1 秒
});
相比而言,筆者更傾向于下面這樣看起來更簡(jiǎn)潔的代碼医增,要知道編程風(fēng)格也是很多面試官重點(diǎn)考察的點(diǎn)慎皱,代碼閱讀時(shí)的顆粒度更小,模塊化更好叶骨,無疑會(huì)是加分點(diǎn)。
const tasks = []; // 這里存放異步操作的 Promise
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, i);
resolve();
}, 1000 * i);
});
// 生成全部的異步操作
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
// 異步操作完成之后祈匙,輸出最后的 i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000);
});
讀到這里的同學(xué)忽刽,恭喜你,你下次面試遇到類似的問題夺欲,至少能拿到 80 分跪帝。
我們都知道使用 Promise 處理異步代碼比回調(diào)機(jī)制讓代碼可讀性更高,但是使用 Promise 的問題也很明顯些阅,即如果沒有處理 Promise 的 reject伞剑,會(huì)導(dǎo)致錯(cuò)誤被丟進(jìn)黑洞,好在新版的 Chrome 和 Node 7.x 能對(duì)未處理的異常給出 Unhandled Rejection Warning市埋,而排查這些錯(cuò)誤還需要一些特別的技巧(瀏覽器黎泣、Node.js)恕刘。
追問 3:ES7
既然你都看到這里了,那就再堅(jiān)持 2 分鐘抒倚,接下來的內(nèi)容會(huì)讓你明白你的堅(jiān)持是值得的褐着。
多數(shù)面試官在決定聘用某個(gè)候選人之前還需要考察另外一項(xiàng)重要能力,即技術(shù)自驅(qū)力托呕,直白的說就是候選人像有內(nèi)部的馬達(dá)在驅(qū)動(dòng)他含蓉,用漂亮的方式解決工程領(lǐng)域的問題,不斷的跟隨業(yè)務(wù)和技術(shù)變得越來越牛逼项郊,究竟什么是牛逼馅扣?建議閱讀程序人生的這篇剖析。
回到正題着降,既然 Promise 已經(jīng)被拿下岂嗓,如何使用 ES7 中的 async await 特性來讓這段代碼變的更簡(jiǎn)潔?你是否能夠根據(jù)自己目前掌握的知識(shí)給出答案鹊碍?請(qǐng)?jiān)谶@里暫停 1 分鐘厌殉,思考下。
下面是筆者給出的參考代碼:
// 模擬其他語(yǔ)言中的 sleep侈咕,實(shí)際上可以是任何異步操作
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { // 聲明即執(zhí)行的 async 函數(shù)表達(dá)式
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
})();
按值傳遞
var output = function (i) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i); // 這里傳過去的 i 值被復(fù)制了
}
console.log(new Date, i);
閉包解決
for (var i = 0; i < 5; i++) {
(function(j) { // j = i
setTimeout(function() {
console.log(new Date, j);
}, 1000);
})(i);
}
console.log(new Date, i);
2017年前端面試題整理匯總100題
最近遇到的前端面試題(2017.02.23更新版)
事件冒泡與事件捕獲公罕,事件委托
0.事件流: 從頁(yè)面接受事件的順序。
1.事件冒泡:是ie處理事件時(shí)耀销,由事件開始的具體的元素接受楼眷,然后逐級(jí)向上傳播到根節(jié)點(diǎn)。
2.事件捕獲: 與事件冒泡恰恰相反熊尉, 接受事件順序?yàn)楦?jié)點(diǎn)到具體點(diǎn)罐柳,這就是事件捕獲。
3.事件委托: 利用事件冒泡狰住,指定一個(gè)事件處理程序张吉,就可以管理某一類事件。
4.addEventListener('事件'催植, ‘函數(shù)’ 肮蛹,false( 或true) 默認(rèn)false是冒泡 true是事件捕獲
前端工作面試問題
proto與prototype的區(qū)別
1. proto是真正用來查找原型鏈去獲取方法的對(duì)象。
2. prototype是在用new創(chuàng)建對(duì)象時(shí)用來構(gòu)建proto的對(duì)象
# Function 和 Object 的區(qū)別
# Function 可以被執(zhí)行
1.Function 可以被執(zhí)行
2.Function 可以當(dāng)做 Object 的構(gòu)造函數(shù)创南,比如當(dāng)我們使用 new操作符后面跟著一個(gè) Function時(shí)伦忠,這個(gè) Function會(huì)被當(dāng)成構(gòu)造函數(shù)返回一個(gè)對(duì)象
那么,javascript 究竟是通過什么來確定繼承關(guān)系的呢稿辙? 答案是 proro
proto和prototype 不同昆码,prototype 只在 Function 中有,而proto則在Function和Object中都有