Three thousand characters, Function.prototype.call is not written

3.thousand characters, Function.prototype.call is not written

Preface

Function.prototype.call
, Handwriting series, Wanwen Interview series, Must meet the content of the series, which shows its weight in the front end.
This article is based on MDN and ECMA standards, and we will re-understand with everyone
call
.

Involved knowledge points:

  1. undefined
  2. void unary operator
  3. Strict mode and non-strict mode
  4. Browser and nodejs environment recognition
  5. Function side effects (pure functions)
  6. eval
  7. Content-Security-Policy
  8. delete
  9. new Function
  10. Object.freeze
  11. Object attribute check
  12. Interview site
  13. Love and hatred between the ECMA specification and browser vendors

Popular version of the Nuggets

Interviewer s question:
please write it by hand

Function.prototype.call

based on
ES6 extended operator
version

Function .prototype.call = function ( context ) { context = context || window ; context[ "fn" ] = this ; let arg = [...arguments].slice( 1 ); context[ "fn" ](...arg); delete context[ "fn" ]; } Copy code

This version should not be the real answer the interviewer wants. Don't do too much analysis.

based on
eval
version of

Function .prototype.call = function ( context ) { context = (context == null || context == undefined )? window : new Object (context); context.fn = this ; var arr = []; for ( var i = 1 ; i < arguments .length; i++) { arr.push( 'arguments[' + i + ']' ); } var r = eval ( 'context.fn(' + arr + ')' ); delete context.fn; return r; } Copy code

This version is worthy of improvement

  1. this
    Is the function not judged
  2. Use undefined to judge, safe or unsafe
    undefined may be rewritten (higher version browsers have made restrictions).
  3. It is too arbitrary to use window directly as the default context.
    Script runtime environment, browser? nodejs?
    Function operation mode, strict mode, non-strict mode?
  4. eval
    Will it be allowed to execute
  5. Does delete context.fn have any side effects
    ? What if the original context has an fn attribute?

Before we actually start to write

Function.prototype.call
Before, let's take a look at how MDN and ECMA define her.

Description of MDN call

syntax

function .call (thisArg, arg1, arg2, ...) Copy the code

parameter

thisArg

Optional. The this value used when the function function is running. Please note that this may not be the actual value seen by this method: if this function is in non-strict mode , it will automatically be replaced by pointing to the global object when it is specified as null or undefined, and the original value will be wrapped .

arg1, arg2, ...

List of specified parameters.

Revealed information

Here are a few pieces of information, which I have marked in bold:

  1. Non-strict mode, corresponding to strict mode
  2. What I said here is pointing to the global object, I didn t say yes
    window
    . Of course, MDN said that it is not a big problem here. What I want to add is
    nodejs
    The ES standard has also been implemented. So when we realize it, do we have to consider
    nodejs
    Environment.
  3. The original value will be wrapped. How to pack it,
    Object(val)
    , That is, the original value
    val
    package of.

ES standard

The ES specification version is listed at the bottom of Function.prototype.call()-JavaScript | MDN , each version has

call implementation
Description.

What we achieve is based on a certain version of ES.

Because the version of ES is different, the implementation details may be different, and the implementation environment is also different.

Specification version status Description
ECMAScript 1st Edition (ECMA-262) Standard Initial definition. Implemented in JavaScript 1.3.
ECMAScript 5.1 (ECMA-262)
Function.prototype.call
Standard
ECMAScript 2015 (6th Edition, ECMA-262)
Function.prototype.call
Standard
ECMAScript (ECMA-262)
Function.prototype.call
Living Standard

In the ES3 standard about

call
The specification is in
11.2.3 Function Calls
, You can find it by searching directly.

Today we are mainly based on the 2009 ES5 standard to achieve

Function.prototype.call
, Some people may say, you, why not implement it under the ES3 standard, because more knowledge points can be involved under ES5.

Undefined undefined

(context == null || context == undefined)? window: new Object(context)

Of the above code

undefined
Not necessarily reliable.

To quote a paragraph from MDN:

In modern browsers (JavaScript 1.8.5/Firefox 4+), undefined is a non-configurable or non-writable attribute since the ECMAscript5 standard. Even if this is not the case, avoid rewriting it.

Used without context

void 0
Than direct use
undefined
More secure.

Some students may have never seen undefined being rewritten. It's okay, here is a picture:

void
In addition to this unary operation, it is ready to return
undefined
In addition, there are two other common uses:

  1. The href of the a tag means nothing

    <a href="javascript:void(0);">

  2. IIFE executes immediately

; void function ( msg ) { console .log(msg) }( "Hello" ); Copy code

Of course the more direct way is:

;( function ( msg ) { console .log(msg) })( "Hello" ); Copy code

Browser and nodejs environment recognition

Browser environment:

typeof Self == 'Object' && self.self === Self duplicated code

nodejs environment:

typeof Global == 'Object' && Global .global === Global duplicated code

Now there is globalThis , which is supported in high version browsers and nodejs.

Obviously, in our scenario, it can't be used yet, but its ideas can be used for reference:

var getGlobal = function () { if ( typeof self !== 'undefined' ) { return self;} if ( typeof window !== 'undefined' ) { return window ;} if ( typeof global !== 'undefined' ) { return global ;} throw new Error ( 'unable to locate global object' ); }; Copy code

Strict mode

Whether to support strict mode

Strict mode is a feature introduced by ES5. So how do we verify that your environment supports strict mode?

var hasStrictMode = ( function () { "use strict" ; return this == undefined ; }()); Copy code

Normal conditions will return

true
, Put it into IE8 to execute:

In non-strict mode, the calling context of the function (the value of this) is the global object. In strict mode, the calling context is undefined.

Is it in strict mode

It is not enough to know whether we support strict mode, we also need to know whether we are in strict mode.

The following code can detect whether it is in strict mode:

var isStrict = ( function () { return this === undefined ; }()); Copy code

This code is in the browser that supports strict mode and

nodejs
All work in the environment.

Function side effects

var R & lt = the eval ( 'context.fn (' + ARR + ')' ); Delete context.fn; duplicated code

The above code directly deletes the context

fn
Attribute, if there is on the original context
fn
Attributes, will they be lost?

We use

eval
Version of
call
, Execute the following code

var context = { fn : "i am fn" , msg : "i am msg" } log.call(context); //i am msg Console .log ( "MSG:" , context.msg); //I AM MSG Console .log ( "Fn:" , context.fn); //Fn: undedined copy the code

You can see the context

fn
The attribute has been killed, which destroys the input parameters and produces side effects that should not be produced.
It is associated with side effects corresponding to the functional programming pure function .

Correspondingly, we have to take action, basically two ideas:

  1. Create an attribute that does not have the same name
  2. Keep the scene and restore the scene

Anything is fine, but I think Option 2 is simpler and easier to implement: the
basic code is as follows:

var ctx = new Object (context); var propertyName = "__fn__" ; var originVal; var hasOriginVal = ctx.hasOwnProperty(propertyName) if (hasOriginVal){ originVal = ctx[propertyName] } ...... //Other code if (hasOriginVal){ ctx[propertyName] = originVal; } Copy code

based on
eval
The realization is basically as follows

Based on the standard ECMAScript 5.1 (ECMA-262) Function.prototype.call

When the call method is called on an object func with argument thisArg and optional arguments arg1, arg2 etc, the following steps are taken: 1. If IsCallable(func) is false , then 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 arg1 append each argument as the last element of argList 4. Return the result of calling the [[Call]] internal method of func, providing thisArg as the this value and argList as the list of arguments . The length property of the call method is 1. NOTE The thisArg value is passed without modification as the this value. This is a Change from Edition . 3 , WHERE A undefined or null thisArg IS REPLACED with The Global Object and ToObject IS OTHER Applied to All values that Result and passed IS AS The the this value. duplicated code

What is more important to us is

1
with
Note
:

Take a look at our basic implementation

var hasStrictMode = ( function () { "use strict" ; return this == undefined ; }()); var isStrictMode = function () { return this === undefined ; }; var getGlobal = function () { if ( typeof self !== 'undefined' ) { return self;} if ( typeof window !== 'undefined' ) { return window ;} if ( typeof global !== 'undefined' ) { return global ;} throw new Error ( 'unable to locate global object' ); }; function isFunction ( fn ) { return typeof fn === "function" ; } function getContext ( context ) { var isStrict = isStrictMode(); if (!hasStrictMode || (hasStrictMode && !isStrict)) { return (context === null || context === void 0 )? getGlobal(): Object (context); } //In strict mode, the compromise solution return Object (context); } Function .prototype.call = function ( context ) { //Cannot be called if ( typeof this !== 'function' ) { throw new TypeError ( this + 'is not a function' ); } //Get context var ctx = getContext(context); //It is safer to create a unique ID and check if there is a duplicate name var propertyName = "__fn__" + Math .random() + "_" + new Date () .getTime (); var originVal; var hasOriginVal = isFunction (ctx.hasOwnProperty)? ctx.hasOwnProperty(propertyName): false ; if (hasOriginVal) { originVal = ctx[propertyName] } ctx[propertyName] = this ; //Use string to splice var argStr = '' ; var len = arguments .length; for ( var i = 1 ; i <len; i++) { argStr += (i === len- 1 )? 'arguments[' + i + ']' : 'arguments[' + i + '],' } var r = eval ( 'ctx["' + propertyName + '"](' + argStr + ')' ); //Restore the scene if (hasOriginVal) { ctx[propertyName] = originVal; } else { delete ctx[propertyName] } return r; } Copy code

The current version still has problems,

  1. In strict mode, we still use
    Obeject
    Encapsulated.

This will lead to the inaccurate point of this when passing non-objects in strict mode, and no compromise is allowed. Anyone who has a better plan, please guide.

  1. Although we have made it difficult to duplicate the name of the temporary property, if the name is duplicated and the method is actually called in the function call, it may cause abnormal behavior.

So the perfect solution is to generate a UID.

  1. eval
    The execution may be Content-Security-Policy to stop

The general prompt information is as follows:

[Report Only] Refused to evaluate a string as JavaScript because'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src ......... Copy code

The first two should be acceptable, and as for the third, we cannot compromise.

This will have to invite the next guest,

new Function
.

new Function

new Function ([arg1[, arg2[, ...argN]],] functionBody)

The basic format is as above, and the last one is the function body.

Give a simple example:

const sum = new Function ( 'a' , 'b' , 'return a + b' ); Console .log (SUM ( 2 , . 6 )); //Output expected:. 8 copy the code

we

call
The number of parameters is not fixed, the idea is from
arguments
Dynamically acquired.

Here, our implementation borrows the interviewer's question: Can you simulate the call and apply methods of JS? Implementation method:

function generateFunctionCode ( argsArrayLength ) { var code = 'return arguments[0][arguments[1]](' ; for ( var i = 0 ; i <argsArrayLength; i++){ if (i> 0 ){ code += ',' ; } code += 'arguments[2][' + i + ']' ; } code += ')' ; //return arguments[0][arguments[1]](arg1, arg2, arg3...) return code; } Copy code

Implementation based on new Function

var hasStrictMode = ( function () { "use strict" ; return this == undefined ; }()); var isStrictMode = function () { return this === undefined ; }; var getGlobal = function () { if ( typeof self !== 'undefined' ) { return self;} if ( typeof window !== 'undefined' ) { return window ;} if ( typeof global !== 'undefined' ) { return global ;} throw new Error ( 'unable to locate global object' ); }; function isFunction ( fn ) { return typeof fn === "function" ; } function getContext ( context ) { var isStrict = isStrictMode(); if (!hasStrictMode || (hasStrictMode && !isStrict)) { return (context === null || context === void 0 )? getGlobal(): Object (context); } //In strict mode, the compromise solution return Object (context); } function generateFunctionCode ( argsLength ) { var code = 'return arguments[0][arguments[1]](' ; for ( var i = 0 ; i <argsLength; i++){ if (i> 0 ){ code += ',' ; } code += 'arguments[2][' + i + ']' ; } code += ')' ; //return arguments[0][arguments[1]](arg1, arg2, arg3...) return code; } Function .prototype.call = function ( context ) { //Cannot be called if ( typeof this !== 'function' ) { throw new TypeError ( this + 'is not a function' ); } //Get context var ctx = getContext(context); //It is safer to create a unique ID and check if there is a duplicate name var propertyName = "__fn__" + Math .random() + "_" + new Date () .getTime (); var originVal; var hasOriginVal = isFunction (ctx.hasOwnProperty)? ctx.hasOwnProperty(propertyName): false ; if (hasOriginVal) { originVal = ctx[propertyName] } ctx[propertyName] = this ; var argArr = []; var len = arguments .length; for ( var i = 1 ; i <len; i++) { argArr[i- 1 ] = arguments [i]; } var r = new Function (generateFunctionCode(argArr.length))(ctx, propertyName, argArr); //Restore the scene if (hasOriginVal) { ctx[propertyName] = originVal; } else { delete ctx[propertyName] } return r; } Copy code

Comment area question collection

The most exciting comment area:

  1. Why not
    Symbol

Because it is written based on the ES5 standard, if you use

Symbol
, The spread operator can also be used. Naturally, the knowledge of the investigation is much less.

  1. Alipay applet evel and new function are not available

In this case, there may be really nothing that can be done.

  1. Objects after Object.freeze cannot add properties

Thank you Xu Kun Naoko for your correction, the difference between his article handwritten call and native Function.prototype.call, I recommend you to read it carefully.

The following code will report an error in strict mode, and copy in non-strict mode is unsuccessful:

"use strict" ; var context = { a : 1 , log ( msg ) { console .log( "msg:" , msg) } }; Object .freeze(context); context.fn = function () { }; console .log(context.fn); VM111 call: 12 Uncaught TypeError : Cannot add property fn, object is not extensible at VM49 call:12

  1. Obect.create

"use strict"; var context = { a: 1, log(msg){ console.log("msg:", msg) } }; Object.freeze(context); var ctx = Object.create(context); ctx.fn = function(){ } console.log("fn:", typeof ctx.fn); //fn: function console.log("ctx.a", ctx.a); //ctx.a 1 console.log("ctx.fn", ctx.fn); //ctx.fn (){}

  1. Object

this

  1. eval
    new Function

  2. call
    this

Function.prototype.call

call

P6,P7 P8

Function.prototype.call() - JavaScript | MDN
Strict mode - JavaScript | MDN
ECMAScript 5 Strict Mode
ES
call apply bind
call apply bind
JS call apply