隐式参数在函数声明中没有明确定义,在函数调用时也没有显示传入,但会默认传递给函数并且可以在函数内正常访问,同时在函数内可以像其他明确定义的参数一样引用它们。
arguments是一个实参容器,它会接收函数调用时传入的所有实参。但要注意的是,它只是一个类数组对象,并不像rest剩余参数一样是Array的实例对象,所以它不能访问Array原型上的方法,但它有length属性并且实现了迭代器接口。
function sum(){ let count = 0; console.log(arguments.length);// 有length属性 for(i of arguments){// 因为实现了迭代器接口,所以可以使用for of count += i } console.log(count) } sum(1,2,3,4,5) 123456789 arguments的元素可以作为对应形参的别名,拥有对该形参的读写能力,但是在严格模式下被禁用,此文不做更多讨论。this参数代表函数调用相关联的对象,通常也称之为函数上下文。在Java中,this通常指向定义当前方法的类的实例,但是在JavaScript中this参数不仅由该函数定义的方式和位置决定,同时还严重受到函数调用方式的影响,接下来即讨论函数的调用方式对函数上下文的影响。
上述三种调用方式,都是由调用规则方式不同而自动根据规则绑定的函数上下文。但是由于JavaScript函数是对象,在作为回调函数调用时,其执行环境和外部词法环境的情况往往不可控(下文会说这两个名词),导致运行时的作用域链非预期,变量搜索出现问题(下文也会说)。所以我们常常有显示控制回调函数的函数上下文的需求,使其作用域链稳定,变量搜索符合预期。
幸运的是,JavaScript早已为我们提供了一种调用函数的方式,从而可以显式地指定任何对象作为函数的上下文,这就是接下来要说的apply方法和call方法。同时因为apply、call是Function原型上的方法,函数都是Function的实例对象,所以我们可以很轻松的通过函数名.apply/call的方式显示绑定上下文并直接调用得到结果。
上文说到,箭头函数的简化不仅仅体现在定义方式上,在调用箭头函数时,不会隐式传入this参数,而是从定义时的函数继承上下文,也即箭头函数的this始终指向函数声明所在的上下文。
// 例1:箭头函数fn在new Test语句后才定义,根据规则,它没有自己的this,而是继承外部的this,并且根据上文所述new方式调用的规则,二者this和new函数返回的结果都指向新的实例对象。 function Test(){ this.fn = () => { return this; } } const t = new Test(); console.log(t.fn() == t); // true // 例2:箭头函数在对象中定义,由于test对象的定义以及解析所在的上下文都在window环境下,所以该箭头函数内部的this指向window const test = { fn: () => { return this; } } console.log(test.fn() == test); // false 12345678910111213141516和apply、call一样,bind也是Function原型上的方法,通过函数名.bind的方式显示绑定this,但与apply、call方法不同的是,bind的调用者函数在绑定this后并不会立即调用,而是仅返回一个绑定this的新的函数。
function Test(){ this.fn = () => { return this; } } const t = new Test() const fn = t.fn.bind(window); console.log(t.fn === fn) // false,证明得到新函数对象 12345678通过上述论证,call和apply不能混用,bind只能在call、apply之前,但其之后通过call和apply再次绑定this则会失效。结论:三者绑定this之间无所谓优先级,因为要么报错,要么没软用。
通过上述代码论证,apply、call、bind方法虽然可以显示绑定函数的this,但是对箭头函数无效,这符合箭头函数没有单独的this,而是继承其定义时所在的函数上下文(也就是外部this指向)的规则。
JavaScript引擎在执行代码时,每一条语句都处于特定的执行上下文中。全局代码在所有函数外部定义,在全局执行上下文环境下执行。函数代码在函数内部定义,在函数执行上下文中(和上文中的this指向的函数上下文不是一个东西,而且一个在栈内一个在堆内)执行。全局执行上下文只有一个,其生命周期从JavaScript程序开始执行到结束。函数执行上下文可以有多个,在每次函数调用时创建,结束后销毁。多个执行上下文之间的关系可以用栈结构表示,如下JavaScript忍者秘籍中案例:
词法环境(lexical environment)是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系,人们也称之为作用域。一个函数、一段代码片段或者try catch语句都可以具有独立的标识符映射表,在ES6之前,JavaScript的词法环境只能与函数关联,没有块级作用域的概念,但在ES6之后,随着let、const的出现才出现了块级作用域的概念。
无论何时创建函数,都会创建一个与之相关联的词法环境,并存储在名为[[Environment]]的内部属性上(也就是说无法直接访问或操作),作为函数对象的属性一起存储在堆中。 无论何时调用函数,都会创建一个新的执行环境,被推入执行上下文栈。除此之外,还会创建一个与外部环境(以全局环境为底的多个词法环境的栈结构)相关联的词法环境(入栈),并且这个词法环境(栈内)会与创建该函数时的词法环境(堆内)进行关联,通过这么一种方式,就实现了作用域链,这是理解闭包的关键。【!!!这里个人理解可能有误,通过chrome实验,并没有发现[[Environment]]属性,而是仅看到[[Scopes]]属性,如若有误,希望看官能够指正,谢谢】
在这个案例中,函数调用的环境与函数定义的环境相同,但事实上,类似回调函数的调用环境常常与其定义环境不同。
标识符搜索就是沿着作用域链这条链来搜索的,从最近的栈内词法环境找到最远的栈内词法环境。如上图查找ninja变量,从report环境向外查到skulk环境,最后搜索到全局环境。
JavaScript代码的执行事实上是分两个阶段进行的。一旦创建了新的词法环境,就会执行第一阶段。在第一阶段,没有执行代码,但是JavaScript引擎会访问并注册在当前词法环境中所声明的变量和函数。JavaScript在第一阶段完成之后开始执行第二阶段,具体如何执行取决于变量的类型(let、var、const和函数声明)以及环境类型 (全局环境、函数环境或块级作用域)。 正是因为词法环境的注册遵从上述规则,才导致了一些操作可行以及怪异的bug:
在函数声明之前可以调用函数 fn() function fn(){console.log('aa')} // 输出aa 12 函数和变量重载由上述过程可知,JavaScript代码在第一阶段的执行当中,函数声明的扫描注册先于变量声明的扫描注册(注意这里说的是函数声明的扫描注册不是函数的扫描注册,以下示例可以论证:
// 以下立即执行函数中,先把fn定义为数字3,而后定义为一个方法,但是在词法环境下的注册顺序却相反,先是识别为函数而后才是数字。 (()=>{ console.log(typeof(fn))// function var fn = 3 function fn(){} console.log(typeof(fn)) // number })() 1234567如果理解了上文所述的执行环境、词法环境、作用域链这三个概念,那么就已经理解闭包了,闭包就是利用了函数调用时创建的词法环境(栈内)会指向函数创建时的词法环境,而如果函数创建时的词法环境内的某个变量(堆内)指向了定义所在环境的某个外部变量(堆内),就会因为这个引用关系,导致外部环境创建的词法环境就不会被回收,进而产生了所谓的闭包,也实现了私有化变量的功能。
内部setInterver的回调函数引用了外部函数animateIt的tick变量,在该timer未被clear之前,timer在等待调用(栈内),该回调函数对象(堆内)始终没有被释放,而它又引用了外部函数animateIt的变量,导致存储animateIt词法环境(堆内)在animateIt运行结束后也被引用,进而无法被释放。直到clear之后,该回调函数对象被释放,存储animateIt的词法环境(堆内)才能被释放。
最后留个闭包经典面试题,如果你能很清楚的明白为什么以下代码的输出为以下图片,那么你的闭包理解可以吊打面试官了。
代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>作用域链</title> </head> <body> <script type="text/javascript"> 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); console.log('------------------------'); var b = fun(0).fun(1).fun(2).fun(3); console.log('------------------------'); var c = fun(0).fun(1); c.fun(2); c.fun(3); </script> </body> </html> 12345678910111213141516171819202122232425262728293031 运行结果