今天在学习前端工程化的过程中,遇到一个是实验中的css属性:fullscreen,有这样一个例子:fullscreen伪元素官方demo

<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>

其中有一段代码:

function enterFullscreen() {fullscreenFunc.call(fullscreenDiv);
} 

虽然结合上下文能看出来是为了兼容浏览器的fullscreen API,但是其中的Function.prototype.call()我自己其实没有特别深究过。

为什么不直接fullscreenFunc(),这样不能使得fullscreenDiv全屏吗?

大家都说call与apply都是为了动态改变this的,仅仅是传入参数的方式不同,call传入(this,foo,bar,baz),而apply传入(this,[foo,bar,baz])那么事实真如大家所说的那样吗?既然apply能动态改变this,那么为什么还要多此一举开放一个call?
这其中肯定隐藏着一些秘密,那就是有些事情是apply做不到,而call可以胜任的。
继续我们的啃规范之旅,去深入到Function.prototype.call()的内部,彻底把它搞清楚。

19.2.3.4 Function.prototype.call (thisArg , ...args)

When the call method is called on an object func with argument, thisArg and zero or more args, the following steps are taken:

  1. If IsCallable(func) is false, throw a TypeError exception.
  2. Let argList be an empty List.
  3. If this method was called with more than one argument then in left to right order, starting with the second argument, append each argument as the last element of argList.
  4. Perform PrepareForTailCall().
  5. Return Call(functhisArgargList).

The length property of the call method is 1.

当call方法在带参数的对象的方法上调用时,thisArg和零个或者对个参数,会进行如下的步骤:

  1. 如果IsCallable(func)返回false,抛出TypeError异常。
  2. 定义argList为一个空的列表。
  3. 如果方法按照从左到右传入的参数个数不止一个,从第二个参数开始,依次将每个参数从尾部添加到argList数组。
  4. 执行PrepareForTailCall()
  5. 返回Call(func,thisArg,argList)

有3个点看不懂:

  • IsCallable(func)
  • PrepareForTailCall()
  • Call(func,thisArg,argList)

这些同样在规范中有对应描述:

7.2.3IsCallable ( argument )

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.

重点在于is a callable function with a [[Call]] internal method.,也就是说执行isCallable(func)运算的func,如果函数内部有一个内在的[[Call]]方法,那么运算结果为true,也就是说这个函数是可调用的的。(callable)

14.6.3Runtime Semantics: PrepareForTailCall ( )

The abstract operation PrepareForTailCall performs the following steps:

  1. Let leafContext be the running execution context.
  2. Suspend leafContext.
  3. Pop leafContext from the execution context stack. The execution context now on the top of the stack becomes the running execution context.
  4. Assert: leafContext has no further use. It will never be activated as the running execution context.

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.

  1. ReturnIfAbrupt(argument).
  2. If Type(argument) is not Object, return false.
  3. If argument has a [[Call]] internal method, return true.
  4. Return false.

虽然看不懂,但还是得硬着头皮学习一波。
抽象操作PrepareForTailCall执行以下几个步骤:

  1. 让叶子上下文成为运行中的执行上下文
  2. 暂停叶子上下文
  3. 顶叶子上下文来自执行上下文的堆。当前的在堆顶部的执行上下文成为运行中的执行上下文
  4. 断言:叶子上下文没有其他作用。它再也不会作为运行中执行上下文被激活。

在调用目标函数或者重用这些资源去支持目标函数之前,尾部位置调用必须释放与当前执行函数上下文相关的瞬态内部资源。

  1. ReturnIfAbrupt(argument).
  2. 如果Type(argument)不是对象,返回false。
  3. 如果argument含有[[call]]内部方法,返回true。
  4. 返回 false

看懂一个大概,是为了在函数调用栈的尾部调用当前函数做准备,其中的运行中执行上下文,正是我们所说的this动态改变的原因,因为本质上this改变并不仅仅是指向的对象发生变化,而是连带着与其相关的上下文都发生了变化。

所以说,这一步是this动态改变的真正原因。

7.3.12Call(F, V, [argumentsList])

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:

  1. ReturnIfAbrupt(F).
  2. If argumentsList was not passed, let argumentsList be a new empty List.
  3. If IsCallable(F) is false, throw a TypeError exception.
  4. Return F.[[Call]](VargumentsList).

Call抽象操作是在调用函数对象的内部的[[Call]]方法。这个操作参数类型包括F,V以及可选的argumentList。F指的是调用函数,V指的是[[Call]]的this值,然后argumentsList是传入到[[Call]]内部方法相应参数的值。如果argumentList不存在,那么argumentList将被置为一个空数组。这个方法按照下列几步执行:

  1. ReturnIfAbrupt(F)
  2. 如果没传入argumentList,那么argumentList将会被置为一个空数组。
  3. 如果IsCallable(F)是false,返回TypeError异常。
  4. 返回 F.[[call]](V,argumentsList).

所以Function.prototype.call(this,...args)执行过程现在很明了:

  1. 判断传入的func是否有[[call]]属性,有[[call]]才意味着函数能被调用,否则抛出TypeError异常。
  2. 定义argList为一个空的列表。
  3. 传参:如果方法按照从左到右传入的参数个数不止一个,从第二个参数开始,依次将每个参数从尾部添加到argList数组。
  4. 切换this上下文:执行PrepareForTailCall(),为函数调用栈在尾部调用函数做准备,切换运行中执行上下文,实现this上下文的动态改变。
  5. 万事具备,执行Call(func,thisArg,argList),调用函数即可。

回到我们的例子:

fullscreenFunc.call(fullscreenDiv);
  1. func为fullscreenDiv DOM 节点的方法:'requestFullscreen' || 'mozRequestFullScreen' || 'msRequestFullscreen'

|| 'webkitRequestFullScreen',由于是fullscreen API,所以isCallable(func)返回true。

  1. 定义一个argList空数组用来传参。
  2. 传参:由于fullscreenFunc.call(fullscreenDiv);只有一个参数,所以直接传入argList空数组。
  3. 切换this上下文:停止当前的this叶子上下文,也就是window,切换到fullscreenDiv的执行上下文。
  4. 由于当前浏览器为chrome,因此执行 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 !

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

  • SegmentFault技术圈:ES新规范语法糖
  • SegmentFault专栏:趁你还年轻,做个优秀的前端工程师
  • 知乎专栏:趁你还年轻,做个优秀的前端工程师
  • Github博客: 趁你还年轻233的个人博客
  • 前端开发交流群:660634678

努力成为优秀前端工程师!

从规范去看Function.prototype.call到底是怎么工作的?相关推荐

  1. h5 plus/h5+规范使用,模块索引,教你如何去看h5+的手册

    最近看了下h5+规范的官网,开始觉得晦涩难懂,确实很乱,不过这也是基于我不理解的情况,终于艰难读完了,现在来分享下心得吧,基本看完文章,按我的方法,应该可以直接上手项目. 我准备的工具 hbuilde ...

  2. 三分钟带你看懂prototype原型——ES6进阶

    三分钟带你看懂prototype原型--ES6进阶 1. prototype 定义 2. new 构造函数 3. 存储 4. prototype 作用 1. prototype 定义 在JS中的类的实 ...

  3. 反正你们写的都是没有即时演示的鸡肋教程,我为什么不去看官方文档?

    By Conmajia 我就拿Vue举个栗子. Vue.js是什么,我想你多半知道,我也甭废话了. 这几天看到写Vue教程的文章突然多起来了.不过,就我所见,在这里发表的所有教程文章,都不支持即时演示 ...

  4. 罗小黑用flash做的_董小姐说电影丨这次都听我的,去看《罗小黑战记》

    董小姐说电影:爱电影的人总会遇到 董文欣:济南百丽宫影城经理.电影资深爱好者 潘娇:FM102.1<体娱潘铎拉>节目主播 潘娇:各位上午好,欢迎来到董小姐说电影,我是主播大娇.有请我们来自 ...

  5. 一起Polyfill系列:Function.prototype.bind的四个阶段

    昨天边参考es5-shim边自己实现Function.prototype.bind,发现有不少以前忽视了的地方,这里就作为一个小总结吧. 一.Function.prototype.bind的作用 其实 ...

  6. Prototype源码浅析——Function.prototype部分(一)

    最近学习都是自己想到什么就些什么,这样进步也不明显,于是偶尔也看看Prototype的源码,分析分析也算笔记. 记得以前看jquery的源码的时候,网上一搜,源码分析一堆,不过透过表面看实质,大部分都 ...

  7. access insert语句怎么写_擦亮自己的眼睛去看SQLServer之简单Insert

    摘要:本来是打算先写SQLServer历史的,不过感觉写那部分内容比较难还需要多查些资料.于是调整了下顺序写下简单的Insert语句. 不过感觉写那部分内容比较难还需要多查些资料.于是调整了下顺序写下 ...

  8. Simulink自动代码生成5——控制函数原型(control function prototype)

    simulink可对生成的代码做深度定制化,主要从以下几个方面: 控制函数原型(function prototypes) 函数接口复用(reuse function interface) 数据存储管理 ...

  9. 抖音直播间怎样避免被封禁,直播间行为规范必看:国仁楠哥

    作为日活跃用户数已经超过6亿的短视频平台,如果因为不知道抖音直播的规则,就把好不容易做起来的账号给弄没了,这真的是太不划算了. 抖音直播间莫名其妙就被封了,被处罚,比如说,由于实名认证与直播间冲突违规 ...

  10. 拿到一个vue+webpack项目,该如何去看

    这也是我目前自己这么理解的,可能每个人看代码的过程也不一样,我这里也是拿到一个项目之后不知道如何开始,所以自己在慢慢摸索,然后记录下来,希望自己能不断超越自己.这里我不能把公司项目亮出来,所以...只 ...

最新文章

  1. ASP.NET Core 模型验证的一个小小坑
  2. 对 C++ 历史的个人观点
  3. 初识Entity Framework CodeFirst(3)
  4. [转]html5 Canvas画图教程(1)—画图的基本常识
  5. 对Docker镜像layer的理解
  6. linux杂谈(十七):iscsi存储分离技术
  7. .NET/C# 优化心得
  8. 一点SICP(Structure and Interpretation of Computer Programs) 资料 (转载加整理)
  9. 整合Mybatis+Spring | 释疑
  10. 公式中的引号怎么输_Excel计数函数中这些奇怪的参数让我百思不得其解!
  11. Julia : string =Int,Float
  12. JavaScript 学习手册二:JS 数据类型
  13. VRay材质练习(一):水、玻璃、牛奶
  14. 图片怎么转文字?建议收藏这些方法
  15. paip QQ音乐导出歌单总结
  16. UBOOT I2C读写详解(基于mini2440)
  17. 百度 google 必应
  18. Windows远程桌面连线显示请稍后
  19. 2021考研数学真题试卷(数学一)
  20. c语言字符串提取数字

热门文章

  1. Python使用matplotlib可视化模拟家庭支出情况雷达图
  2. python代码如何做成应用程序_如何发布你的Python应用程序
  3. 数据库之SQL(该列没有包含在聚合函数或 GROUP BY 子句中)
  4. Java使用S3的一些操作
  5. [R语言绘图]直方图hist
  6. MACOS,应用签名后就崩溃?
  7. 六石管理学:谈管理的前提,要有一点的胸量
  8. i7-9700与E5 2667 V2空载功耗对比
  9. JAVA给枚举成员指定值
  10. Graphics进行局部旋转的办法