首页 > 其他分享 >从规范去看Function.prototype.call到底是怎么工作的?

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

时间:2022-10-07 23:05:06浏览次数:59  
标签:Function function 上下文 fullscreenFunc fullscreenDiv call Call prototype

今天在学习前端工程化的过程中,遇到一个是实验中的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);

其中有一段代码:

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) isfalse, throw aTypeErrorexception.
  2. LetargListbe 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 ofargList.
  4. Perform​​PrepareForTailCall​​().
  5. Return​​Call​​(func,thisArg,argList).

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.3​​IsCallable ( argument )

The abstract operation IsCallable determines if argument, which must be an ​​ECMAScript language value​​​or 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.3​​Runtime Semantics: PrepareForTailCall ( )

The abstract operation PrepareForTailCall performs the following steps:

  1. LetleafContextbe​​the running execution context​​.
  2. ​Suspend​leafContext.
  3. PopleafContextfrom​​the execution context stack​​​. The​​execution context​​​ now on the top of the stack becomes​​the running execution context​​.
  4. ​Assert​​:leafContexthas 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, returnfalse.
  3. Ifargumenthas a [[Call]] internal method, returntrue.
  4. Returnfalse.

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

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

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

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

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

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

​7.3.12​​Call(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. IfargumentsListwas not passed, letargumentsListbe a new empty​​List​​.
  3. If​​IsCallable​​(F) isfalse, throw aTypeErrorexception.
  4. ReturnF.​​[Call]​​.

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

从规范去看Function.prototype.call到底是怎么工作的?_执行上下文

而我们是想触发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(this, 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}
// 回调函数中强制指定函数上下文
// 手动实现一个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实例。

  • 微信公众号: 大大大前端

标签:Function,function,上下文,fullscreenFunc,fullscreenDiv,call,Call,prototype
From: https://blog.51cto.com/u_15725382/5735195

相关文章