今天在学习前端工程化的过程当中,遇到一个是实验中的css属性:fullscreen
,有这样一个例子:fullscreen伪元素官方demojavascript
<div id="fullscreen"> <h1>:fullscreen Demo</h1> <p>This text will become big and red when the browser is in fullscreen mode.</p> <button id="fullscreen-button">Enter Fullscreen</button> </div>
<script> var fullscreenButton = document.getElementById("fullscreen-button"); var fullscreenDiv = document.getElementById("fullscreen"); var fullscreenFunc = fullscreenDiv.requestFullscreen; if (!fullscreenFunc) { ['mozRequestFullScreen', 'msRequestFullscreen','webkitRequestFullScreen'].forEach(function (req) { fullscreenFunc = fullscreenFunc || fullscreenDiv[req]; }); } function enterFullscreen() { fullscreenFunc.call(fullscreenDiv); } fullscreenButton.addEventListener('click', enterFullscreen); </script>
其中有一段代码:css
function enterFullscreen() { fullscreenFunc.call(fullscreenDiv); }
虽然结合上下文能看出来是为了兼容浏览器的fullscreen API,可是其中的Function.prototype.call()我本身其实没有特别深究过。前端
为何不直接fullscreenFunc(),这样不能使得fullscreenDiv全屏吗?vue
你们都说call与apply都是为了动态改变this的,仅仅是传入参数的方式不一样,call传入(this,foo,bar,baz),而apply传入(this,[foo,bar,baz])那么事实真如你们所说的那样吗?既然apply能动态改变this,那么为何还要画蛇添足开放一个call?
这其中确定隐藏着一些秘密,那就是有些事情是apply作不到,而call能够胜任的。
继续咱们的啃规范之旅,去深刻到Function.prototype.call()的内部,完全把它搞清楚。java
When the
call
method is called on an object func with argument, thisArg and zero or more args, the following steps are taken:
The length
property of the call
method is 1.git
当call方法在带参数的对象的方法上调用时,thisArg和零个或者对个参数,会进行以下的步骤:es6
有3个点看不懂:github
这些一样在规范中有对应描述:web
The abstract operation IsCallable determines if argument, which must be an ECMAScript language valueor a Completion Record, is a callable function with a [[Call]] internal method.chrome
重点在于is a callable function with a [[Call]] internal method.,也就是说执行isCallable(func)运算的func,若是函数内部有一个内在的[[Call]]方法,那么运算结果为true,也就是说这个函数是可调用的的。(callable)
The abstract operation PrepareForTailCall performs the following steps:
A tail position call must either release any transient internal resources associated with the currently executing function execution context before invoking the target function or reuse those resources in support of the target function.
虽然看不懂,但仍是得硬着头皮学习一波。
抽象操做PrepareForTailCall执行如下几个步骤:
在调用目标函数或者重用这些资源去支持目标函数以前,尾部位置调用必须释放与当前执行函数上下文相关的瞬态内部资源。
看懂一个大概,是为了在函数调用栈的尾部调用当前函数作准备,其中的运行中执行上下文,正是咱们所说的this动态改变的缘由,由于本质上this改变并不只仅是指向的对象发生变化,而是连带着与其相关的上下文都发生了变化。
因此说,这一步是this动态改变的真正缘由。
The abstract operation Call is used to call the [[Call]] internal method of a function object. The operation is called with arguments F, V , and optionally argumentsList where F is the function object, V is an ECMAScript language value that is the this value of the [[Call]], and argumentsList is the value passed to the corresponding argument of the internal method. If argumentsList is not present, an empty List is used as its value. This abstract operation performs the following steps:
Call抽象操做是在调用函数对象的内部的[[Call]]方法。这个操做参数类型包括F,V以及可选的argumentList。F指的是调用函数,V指的是[[Call]]的this值,而后argumentsList是传入到[[Call]]内部方法相应参数的值。若是argumentList不存在,那么argumentList将被置为一个空数组。这个方法按照下列几步执行:
F.[[call]](V,argumentsList)
.因此Function.prototype.call(this,...args)执行过程如今很明了:
回到咱们的例子:
fullscreenFunc.call(fullscreenDiv);
|| 'webkitRequestFullScreen',因为是fullscreen API,因此isCallable(func)返回true。
fullscreenDiv.webkitRequestFullscreen.[[call]](this,[])
。所以咱们以前提的那个为何不直接fullscreenFunc(),这样不能使得fullscreenDiv全屏吗?,答案就很清楚了?不能。
为何呢?
var fullscreenFunc = fullscreenDiv.requestFullscreen; if (!fullscreenFunc) { ['mozRequestFullScreen', 'msRequestFullscreen','webkitRequestFullScreen'].forEach(function (req) { fullscreenFunc = fullscreenFunc || fullscreenDiv[req]; }); }
下面的代码,仅仅是得到了fullscreenDiv对象的fullscreen request API的引用,而fullscreenFunc的做用域是全局的window对象,也就是this的当前指向为window。
而咱们是想触发window的子对象fullscreenDiv的全屏方法,因此须要将this上下文切换为fullscreenDiv,这就是不直接调用fullscreenFunc(),须要fullscreenFunc.call(fullscreenDiv)的缘由。
最近在看龙书,第一章讲到动态语言与静态语言的区别,龙书中讲到"运行时决定做用域的语言是动态语言,在编译时指定做用域的预言是静态语言"。例子中的以function关键字定义的类,this运行中执行上下文的切换,偏偏证实了javascript是一门动态语言;再举个形象的静态语言的例子,java会使用class关键字构建类,在类内部使用private,public等关键字去指定做用域,编译时就会去约束其做用域,具备很是强的约束性,this始终指向当前类。
刚才和一个java后端同事确认,java也有this关键字,可是仅能使用当前类中的方法,B类能够调用A类中的方法,好比经过super实现对父类的继承,可是当前类中的this指向是不会变的。
js中的this,是能够经过call或者apply进行动态切换从而去调用其余类中的方法的,B类不能调用A类中的方法。(注意:咱们这里的类指的是以function关键字进行定义的类,暂时不考虑es6的class关键字构造类的方式。)
说了这么多,咱们再来强调下重点:
加粗的部分是重点!
加粗的部分是重点!
加粗的部分是重点!
抛开V8引擎内部执行call和apply的原理不说,两者最终实现的都是this上下文的动态切换,因此就像你们所说的那样,都是动态改变this。咱们只要内心知道,其实两者在背后实现动态切换this的操做部分有很大的不一样就能够了,当出现因为内部实现细节引发的问题时,咱们能够快速定位。
That's it !
2019.8.20更新
js忍者秘籍给出的精简解释是:“js能够经过apply和call显示指定任意对象做为其函数上下文。”强烈建议阅读P52~P55。言简意赅,通俗易懂。
主要有两个用途:
回调函数强制指定函数上下文很好地体现了函数式编程的思想,建立一个函数接收每一个元素,而且对每一个元素作处理。
本质上,apply和call都是为了加强代码的可扩展性,提高编程的效率。
我想这也是js中每个方法或者api的初衷,提供更加便利的操做,解放初更多的生产力。不断加入新方法的es规范也是这个初衷。
因为我使用vue比较多,因此根据以上的应用场景出1个单文件组件示例和1个普通示例供参考:
// 普通函数中指定函数上下文 // 经过Math.max()得到数组中的最大项 <script> function maxNumber(...args) { this.maxNumber = Math.max.apply(null, args); } export default { data() { return { applyTest: { numbers: [1, 2, 4, 5, 3], }, } }, created() { maxNumber.apply(this.applyTest, this.applyTest.numbers); console.log(this.applyTest); // {"numbers": [1,2,4,5,3],"maxNumber": 5} }, } </script>
// 回调函数中强制指定函数上下文 // 手动实现一个Array.prototype.filter const numbers = [1, 2, 3, 4]; function arrayFilter(array, callback) { const result = []; for (let i = 0; i < array.length; i++) { const validate = callback.call(array[i], array[i]); if (validate) { result.push(array[i]); } } return result; } const evenArrays = arrayFilter(numbers, (n) => n % 2 === 0); console.log(evenArrays);// [2, 4]
2019.8.23更新
在上面的示例中,本质上是call,apply实现伪造对象继承。
this.applyTest伪造继承了MaxNumber类,从而新建出单独包含maxNumber属性的实例。
array[i]伪造继承了callback类,从而新建出每个传入参数以后的validate实例。
期待和你们交流,共同进步,欢迎你们加入我建立的与前端开发密切相关的技术讨论小组:
- SegmentFault专栏:趁你还年轻,作个优秀的前端工程师
- Github博客: 趁你还年轻233的我的博客
- 掘金主页:趁你还年轻233
- SegmentFault技术圈: ES新规范语法糖
- 知乎专栏:趁你还年轻,作个优秀的前端工程师
- 前端开发交流群:660634678
努力成为优秀前端工程师!
加油,前端同窗们!