preface

Function. Prototype. Call, handwritten series, ten million text interview series, will contain the content of the series must include, it is in the front end of the weight. This article is based on THE MDN and ECMA standards, and we reunderstand call.

Related knowledge:

  1. undefined
  2. Void unary operator
  3. Strict mode and non-strict mode
  4. Browser and NodeJS environment identification
  5. Function side effects (pure functions)
  6. eval
  7. Content-Security-Policy
  8. delete
  9. new Function
  10. Object.freeze
  11. Object property checking
  12. The interview scene
  13. The love/hate relationship between the ECMA specification and browser vendors

The popular version of the nuggets

Interviewer question: Please write it by hand

Based on theES6 extension operatorversion

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

This version is probably not the answer the interviewer is looking for. Not a lot of parsing.

Based on theevalThe version of the

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

This version is worth improving

  1. thisStudent: Is the function not judging
  2. Use undefined for judgment. Undefined can be overwritten if it is not safe (this is already restricted by higher browser versions).
  3. Using window as the default context is arbitrary. Script runtime environment, browser? Nodejs? Function running mode, strict mode, non-strict mode?
  4. evalWill it be allowed to go ahead
  5. Does delete context.fn have any side effects if there was an fn property on the context

Before we actually write function.prototype. call, let’s take a look at how MDN and ECMA define it.

MDN Call description

grammar

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

parameter

thisArg

Optional. The value of this used when function is run. Note that this may not be the actual value that the method sees: if the function is in non-strict mode, null or undefined is automatically replaced with a reference to the global object, and the original value is wrapped. arg1, arg2, … Parameter list to be specified.

Disclosed information

Here are a few things I’ve highlighted in bold:

  1. The non-strict mode corresponds to the strict mode
  2. Point to a global objectwindow. Of course, MDN here said window is not too big problem. I would like to add thatnodejsIt also implements the ES standard. So when we implement, do we have to think aboutnodejsThe environment?
  3. Raw values are wrapped. How about the packing?Object(val)That is, the original value is completedvalThe packaging.

ES standard

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

We’re going to implement it based on a version of ES.

Because of the different versions of ES, the implementation details may be different, and the implementation environment may be different.

Specification version state instructions
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

The ES3 specification for call is found in 11.2.3 Function Calls.

Today we are implementing function.prototype. Call based on the 2009 ES5 standard. Some people might say, “Why don’t you implement it in ES3?

Unreliable undefined

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

Undefined in the above code is not necessarily reliable.

A quote from MDN:

In modern browsers (JavaScript 1.8.5/Firefox 4+), undefined is a property that cannot be configured or rewritten without additional information, and is freely configurable and cannot be rewritten, since the ECMAscript5 standard. Avoid trying to rewrite it even if it isn’t.

Using void 0 without context is safer than using undefined directly.

For those of you who haven’t seen undefined rewritten, that’s fine, let me draw a picture:

The void unary operation has two other common uses in addition to the preparation to return undefined:

  1. The IIFE is executed immediately

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

Copy the code

Of course, the more direct way is:

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

Browser and NodeJS environment identification

Browser environment:

typeof self == 'object' && self.self === self
Copy the code

Nodejs environment:

typeof global= ='object' && global.global === global
Copy the code

GlobalThis is already available and is supported in both higher browser versions and nodeJS.

Obviously, in our scenario, it won’t work, but the idea can be used:

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

Strict mode

Whether to support strict mode

Strict Mode Specifies the Strict mode, which is introduced by ES5. So how do we verify that your environment supports strict mode?

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

Returns true in normal cases, and executes in IE8:

In non-strict mode, the function’s invocation context (the value of this) is a global object. In strict mode, the invocation context is undefined.

Whether the system is in strict mode

It’s not enough to know if we’re in strict mode, we also need to know if we’re in strict mode.

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

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

This code works both in strict mode supported browsers and in nodeJS environments.

Function side effect

  var r = eval('context.fn(' + arr + ') ');
  delete context.fn;
Copy the code

The above code simply removes the fn property from the context. If the original context had the FN property, would that have been lost?

We take the eval version of the call and 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 that the fn property of the context has been killed, destroying the input, which has unintended side effects. The counterpart to side effects are pure functions in functional programming.

In response, we should take action in two basic ways:

  1. Make a property that doesn’t have the same name
  2. Preserve the scene and restore the scene

Either is ok, but I think solution 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 the code

Based on theevalThe implementation of the basic as follows

Based on 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 applied to all other values and that result is passed 
as the this value.
Copy the code

What is important to us is 1 and Note:

Take a look at our base implementation

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

var getGlobal = function () {
    if (typeofself ! = ='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, compromise
    return Object(context);
}

Function.prototype.call = function (context) {

    // Cannot be called
    if (typeof this! = ='function') {
        throw new TypeError(this + ' is not a function');
    }
    
    // Get the context
    var ctx = getContext(context);

    // It is safer to create a unique ID and check whether the name is the same
    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 concatenation
    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 the code

There are still problems with the current version,

  1. In strict mode, we still use itObejectIs encapsulated.

This will result in a compromise when passing nonobjects in strict mode. Who has a better plan, please guide.

  1. Although we have made temporary property names difficult to duplicate, if the same name is used and the method is actually called in a function call, it may result in abnormal behavior.

So the perfect solution is to produce a UID.

  1. evalThe execution may beContent-Security-Policystop

The general 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 the code

The first two items should be acceptable. As for the third item, we can’t compromise.

Which brings us to our next guest, new Function.

new Function

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

The basic format is as above, with the last one being the body of the function.

Here’s a simple example:

const sum = new Function('a'.'b'.'return a + b');

console.log(sum(2.6));
// expected output: 8
Copy the code

The number of call arguments is not fixed. The idea is to dynamically obtain arguments from the call parameter.

Can you simulate the implementation of JS call and apply methods?

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

Implementation based on new Function

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

var getGlobal = function () {
    if (typeofself ! = ='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, compromise
    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 the context
    var ctx = getContext(context);

    // It is safer to create a unique ID and check whether the name is the same
    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 the code

Questions collected in the comments section

The comments section highlights:

  1. Why notSymbol

Since it is written according to the ES5 standard, if Symbol is used, the extension operator can also be used. The scope of knowledge is naturally much less.

  1. Alipay small program evel, new Function are not to use

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

  1. You cannot add properties to an Object after object. freeze

Thank you for the correction of Imaginary Kun CAI nizi. The difference between handwritten call and native function.prototype. Call is recommended for you to read closely.

The following code will report an error in strict mode and will not copy successfully in non-strict mode:

"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
Copy the code

I can think of two ways to do this:

  1. Copy the object
  2. Obect.create

This is a compromise, since the link is getting longer.

"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 ƒ () {}
Copy the code

summary

Review the remaining issues

  1. In strict mode, we still need to useObjectEncapsulate the underlying data types

This will result in a compromise when passing nonobjects in strict mode. Who has a better plan, please guide.

  1. Although we have made temporary property names difficult to duplicate, if the same name is used and the method is actually called in a function call, it may result in abnormal behavior

  2. Environments such as small programs may prohibit the use of eval and new functions

  3. Object is frozen, and this in the call execution function is not the actual context object passed in.

So, I still changed the title to 3000 words, and I didn’t write function.prototype.call

The interview scene

A handwritten call involves a lot of knowledge, my level is limited, if there is any omission, please understand and add.

When the interviewer asks you a question, you should know if you are interviewing for a P6,P7 or P8 position. Is it senior development or front end leader or front end leader. Interviewers expect different answers depending on the position.

Write in the last

Writing is not easy, your support is the biggest motivation for me to move forward.

References and references

Call () JavaScript Function. The prototype. | MDN Strict mode – JavaScript | MDN ECMAScript 5 Strict mode ES collection Interviewer: can you simulate the implementation of JS call and apply methods