仕事から発展した面接の質問
これは仕事中に遭遇した質問で、とても面白そうだったので面接の質問にしました。全問正解して理由を言える人はほとんどいなかったので取り上げて話しました。それについて。
最初に質問コードを見てください:
function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,? var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,? var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,? //问:三行a,b,c的输出分别是什么?
これは非常に典型的な JS クロージャーの問題です。そこには 3 つのレベルの楽しい関数がネストされています。各レベルの楽しい関数がどの関数であるかを理解することが特に重要です。
まず自分が考えた結果を紙などに書き出して、それを広げて正解を確認してみてはいかがでしょうか?
答え
//a: undefined,0,0,0 //b: undefined,0,1,2 //c: undefined,0,1,1
すべて正しく理解できましたか?すべて正しく答えられた場合は、おめでとうございます。答えがない場合でも、JS クロージャの問題でつまずく可能性のあるものはほとんどありません。
JS にはいくつかの関数があります
まず、その前に理解しておく必要があるのは、JSの関数は名前付き関数(名前付き関数)と匿名関数の2種類に分けられるということです。
この 2 つの関数の区別方法は非常に簡単で、fn.name を出力することで名前が付けられている関数、名前が付けられていない関数が匿名関数であると判断できます。
注: 以前のバージョンの IE では、名前付き関数の名前を取得できないため、未定義が返されます。Firefox または Google Chrome でテストすることをお勧めします。
または、IE 互換の関数名取得メソッドを使用して関数名を取得します。
/** * 获取指定函数的函数名称(用于兼容IE) * @param {Function} fun 任意函数 */ function getFunctionName(fun) { if (fun.name !== undefined) return fun.name; var ret = fun.toString(); ret = ret.substr('function '.length); ret = ret.substr(0, ret.indexOf('(')); return ret; }
変数 fn1 は名前付き関数、fn2 は匿名関数であることがわかります
関数を作成するいくつかの方法
関数の種類について説明した後は、JS で関数を作成する方法がいくつかあることも理解する必要があります。
1. 関数の宣言
関数名と関数本体を含む、関数を宣言する最も一般的で標準的な方法。
関数 fn1(){}
2. 匿名関数式を作成します
内容が関数である変数を作成します
var fn1=function (){}
このメソッドを使用して作成された関数は匿名関数であることに注意してください。つまり、関数名
がありません。
var fn1=function (){}; getFunctionName(fn1).length;//0
3. 名前付き関数式を作成します
名前付きの関数を含む変数を作成します
var fn1=function xxcanghai(){};
注: 名前付き関数式の関数名は、作成された関数内でのみ使用できます
内でのみ使用できます。
テスト:
var fn1=function xxcanghai(){ console.log("in:fn1<",typeof fn1,">xxcanghai:<",typeof xxcanghai,">"); }; console.log("out:fn1<",typeof fn1,">xxcanghai:<",typeof xxcanghai,">"); fn1(); //out:fn1< function >xxcanghai:< undefined > //in:fn1< function >xxcanghai:< function >
注: var o={ fn : function (){…} } などのオブジェクト内での関数の定義も関数式です
4. 関数コンストラクター
関数文字列を Function コンストラクターに渡し、この文字列コマンドを含む関数を返すことができます。このメソッドは匿名関数を作成します。
5. 自己実行関数
(function(){alert(1);})(); (function fn1(){alert(1);})();
6. 関数を作成するその他の方法
もちろん、関数を作成したり実行したりする方法は他にもあります。たとえば、eval、setTimeout、setInterval などの非常に一般的なメソッドを使用します。これらは非標準的な方法なので、ここでは詳しく説明しません。
3 つの楽しい関数の関係は何ですか?
関数の種類と関数の作成方法について話した後、本題に戻り、このインタビューの質問を見てください。このコードには 3 つの楽しい関数があるため、最初のステップは、これら 3 つの楽しい関数間の関係と、どの関数がどの関数と同じであるかを理解することです。
function fun(n,o) { console.log(o) return { fun:function(m){ //... } }; }
先看第一个fun函数,属于标准具名函数声明,是新创建的函数,他的返回值是一个对象字面量表达式,属于一个新的object。
这个新的对象内部包含一个也叫fun的属性,通过上述介绍可得知,属于匿名函数表达式,即fun这个属性中存放的是一个新创建匿名函数表达式。
注意:所有声明的匿名函数都是一个新函数。
所以第一个fun函数与第二个fun函数不相同,均为新创建的函数。
函数作用域链的问题
再说第三个fun函数之前需要先说下,在函数表达式内部能不能访问存放当前函数的变量。
测试1:对象内部的函数表达式:
var o={ fn:function (){ console.log(fn); } }; o.fn();//ERROR报错
测试2:非对象内部的函数表达式:
var fn=function (){ console.log(fn); }; fn();//function (){console.log(fn);};正确
结论:使用var或是非对象内部的函数表达式内,可以访问到存放当前函数的变量;在对象内部的不能访问到。
原因也非常简单,因为函数作用域链的问题,采用var的是在外部创建了一个fn变量,函数内部当然可以在内部寻找不到fn后向上册作用域查找fn,而在创建对象内部时,因为没有在函数作用域内创建fn,所以无法访问。
所以综上所述,可以得知,最内层的return出去的fun函数不是第二层fun函数,是最外层的fun函数。
所以,三个fun函数的关系也理清楚了,第一个等于第三个,他们都不等于第二个。
到底在调用哪个函数?
再看下原题,现在知道了程序中有两个fun函数(第一个和第三个相同),遂接下来的问题是搞清楚,运行时他执行的是哪个fun函数?
function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,? var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,? var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,? //问:三行a,b,c的输出分别是什么?
1. 最初の行 a
var a = fun(0); a.fun(3);
最初の fun(0) が第 1 レベルの fun 関数を呼び出していることがわかります。 2 番目の fun(1) は、前の fun の戻り値を呼び出す fun 関数です。つまり、次のようになります。
最後のいくつかの fun(1)、fun(2)、fun(3) 関数はすべて、第 2 レベルの fun 関数を呼び出しています。
その後:
fun(0) が初めて呼び出されたとき、o は未定義です。
fun(1) が 2 回目に呼び出されるとき、m は 1 です。このとき、fun は外側の関数の n を閉じます。つまり、最初の呼び出しでは n=0、つまり m=1, n =0 で、最初のレベルの fun 関数 fun(1,0) が内部的に呼び出されるため、o は 0 になります。fun(2) が 3 回目に呼び出されるとき、m は 2 ですが、a.fun はまだ呼び出されているため、最初の呼び出しの n はまだ閉じられているため、fun(2,0) の最初の層が呼び出されます。内部的には、o は 0
です。4 回目と同じ;
つまり、最終的な答えは未定義、0,0,02. 2 行目 b
var b = fun(0).fun(1).fun(2).fun(3);//未定義,?,?,?
fun(0) から始めましょう。これは呼び出される第 1 レベルの fun 関数である必要があり、その戻り値はオブジェクトであるため、2 番目の fun(1) は第 2 レベルの fun 関数を呼び出します。次のいくつかは、呼び出される第 2 レベルの楽しい関数でもあります。
その後:
最初のレベル fun(0) が初めて呼び出されるとき、o は未定義です。
.fun(2) が 3 回目に呼び出されるとき、m は 2 です。このとき、現在の fun 関数は 1 回目の実行の戻りオブジェクトではなく、2 回目の実行の戻りオブジェクトです。第 1 レベルの fun 関数が 2 回目に実行されるとき (1,0)、つまり n=1、o=0、2 番目の n は戻るときに閉じられるため、第 3 レベルの fun 関数が 3 番目に呼び出されるとき時間、m =2,n=1、つまり第 1 レベルの fun 関数 fun(2,1) が呼び出されるので、o は 1 になります。
.fun(3) が 4 回目に呼び出されるとき、m は 3 であり、これで 3 回目の呼び出しの n が閉じられます。同様に、第 1 レベルの fun 関数の最後の呼び出しは fun(3,2) になります。 o は 2;
それが最終的な答えです: 未定義,0,1,2
3. 3 行目 c
var c = fun(0).fun(1); c.fun(2); //未定義,?,?,?
前の 2 つの例に基づいて、次のことがわかります。
fun(0) は第 1 レベルの fun 関数を実行し、.fun(1) は fun(0) によって返された第 2 レベルの fun 関数を実行します。ステートメントはここで終了し、c は fun(1) の戻り値を格納します。 、 fun(0) の戻り値ではなく、 c のクロージャは fun(1) が 2 回目に実行されるときの n の値でもあります。 c.fun(2) は fun(1) によって返される第 2 レベルの fun 関数を実行し、c.fun(3) も fun(1) によって返される第 2 レベルの fun 関数を実行します。
その後:
最初のレベル fun(0) が初めて呼び出されるとき、o は未定義です。
.fun(1) が 2 回目に呼び出されるとき、m は 1 です。このとき、fun は外側の関数の n を閉じます。つまり、最初の呼び出しでは n=0、つまり m=1、 n=0 で、最初のレベルの fun 関数 fun(1,0) が呼び出されるので、o は 0 になります。
4 回目の .fun(3) にも同じことが当てはまりますが、これはまだ 2 回目の呼び出しの戻り値であるため、最終的に最初のレベルの fun 関数 fun(3,1) が呼び出されるため、o は 1 のままです。
それが最終的な答えです: 未定義,0,1,1あとの言葉
このコードは元々、非同期コールバックを同期呼び出しコンポーネントに書き換えたときに作成されたもので、私はこの落とし穴を発見し、JS クロージャについての理解を深めました。
インターネット上にはクロージャとは何かについての記事が無数にありますが、クロージャとは何かを理解するには、コード内でクロージャを自分で見つけて理解する必要があります。
クロージャとは何かと聞かれたら、広義のクロージャとは、変数を独自のスコープ内で使用することを指し、これをクロージャと呼びます。 みんなは正解できましたか?この記事を通じて、読者がクロージャ現象についてより深く理解できることを願っています。他に洞察や意見がある場合は、お気軽に修正して議論してください。