A lot of people who are new to JavaScript say to me, “What if I can’t remember a lot of JavaScript apis? Array method, that method is always silly not clear, what to do? It’s frustrating to remember how to manipulate the DOM today and forget it tomorrow.”

I’ve even had developers complain to me during interviews: “Interviewers are so obsessed with API usage, they even need me to explain the order of parameters in some jQuery methods!”

I think all of us need to be “rote memorized”, able to write backwards, for repeated methods. Some apis that never seem to be remembered are simply not used enough.

As an interviewer, I never force developers to recite the API exactly. Instead, I like to look at candidates the other way: “I can’t remember how to use it, so I’ll tell you how to use it and you implement it!” Implementing an API can not only test the interviewer’s understanding of the API, but also reflect the developer’s programming thinking and code ability. For motivated front-end engineers, it should be “commonplace” to imitate and implement some classic methods. This is a relatively basic requirement.

** In this section, I select several typical apis based on the interview questions I know and my experience as an interviewer, and cover some knowledge points and programming essentials in JavaScript by implementing them in different degrees and in different ways. ** By studying this section, I hope you will not only understand the meaning of the code, but also learn how to draw inferences.

The API topics are as follows:

JQuery offset implementation

This topic evolved from a front exam question in today’s headlines. The interviewer asked: “How do I get the distance between any element in the document and the top of the document?”

Those familiar with jQuery will be familiar with the offset method, which returns or sets the offset (position) of the matched element relative to the document. The object returned by this method contains two integer attributes, top and left, in pixels. If you can use jQuery, you can retrieve the results directly from the API. But what if you implement it in native JavaScript, that is, manually implement the jQuery offset method?

There are two main ideas:

  • We do it recursively
  • throughgetBoundingClientRectAPI implementation

Recursive implementation scheme

We do this by traversing the target element, the parent of the target element, and the parent’s parent…… Trace the source sequence, and sum the offset of these traversed nodes with respect to their nearest ancestor (and position property is non-static), up to document, to obtain the result.

Here, we need to use JavaScript offsetTop to access the vertical offset of the ancestor element on a DOM node whose border is closest to itself and whose position is non-static. Concrete implementation is as follows:

const offset = ele => {
    letResult = {top: 0, left: 0} // Display === of current DOM node'none', return {top: 0, left: 0}if (window.getComputedStyle(ele)['display'= = ='none') {
        return result
    }
 
    let position

    const getOffset = (node, init) => {
        if(node.nodeType ! = = 1) {return
        }

        position = window.getComputedStyle(node)['position']
 
        if (typeof(init) === 'undefined' && position === 'static') {
            getOffset(node.parentNode)
            return
        }

        result.top = node.offsetTop + result.top - node.scrollTop
        result.left = node.offsetLeft + result.left - node.scrollLeft
 
        if (position === 'fixed') {
            return
        }
 
        getOffset(node.parentNode)
    }
 
    getOffset(ele, true)
 
    return result
}
Copy the code

The above code is not difficult to understand and is implemented recursively. Node.nodetype is not Element(1). If the position attribute of the relevant node is static, the calculation is not counted and the recursion of the next node (its parent) is entered. If the display attribute of the related attribute is None, 0 should be returned as the result.

This implementation is a good test of the developer’s beginner level of recursion and mastery of JavaScript methods.

Next, we implement the jQuery offset method with a relatively new API: getBoundingClientRect.

getBoundingClientRectmethods

The getBoundingClientRect method is used to describe the location of an element where the following four properties are relative to the position in the upper-left corner of the viewport. Execute this method on a node and its return value is an object of type DOMRect. This object represents a rectangular box with read-only properties like: left, top, right, and bottom.

Please refer to implementation:

const offset = ele => {
    letResult = {top: 0, left: 0}if(! ele.getClientRects().length) {returnResult} // Display === of the current DOM node'none', return {top: 0, left: 0}if (window.getComputedStyle(ele)['display'= = ='none') {
        return result
    }

    result = ele.getBoundingClientRect()
    var docElement = ele.ownerDocument.documentElement

    return {
        top: result.top + window.pageYOffset - docElement.clientTop,
        left: result.left + window.pageXOffset - docElement.clientLeft
    }
}
Copy the code

The details to pay attention to are:

  • Node. OwnerDocument. DocumentElement usage may everyone is strange, ownerDocument is an attribute of the DOM node, it returns a document object at the top of the current node. OwnerDocument is the document, and documentElement is the root node. In fact, ownerDocument contains two nodes:

    • <! DocType>
    • documentElement

    Docelement. clientTop, clientTop is the width of the top border of an element, not including top margins or inside margins.

  • In addition, the method implementation is simple geometry, boundary case and compatibility processing, is not difficult to understand.

As the title suggests, such an implementation makes more sense than looking at a “rote memorization” API. From the interviewer’s point of view, I tend to give the interviewer (developer) a method tip that leads to the final solution implementation.

Related implementation of array Reduce method

The array method is very important: ** Because arrays are data, data is state, and state reflects the view. ** We are familiar with array operations, and the reduce method is especially handy. I think this method is a good embodiment of the concept of “functional”, and it is one of the most popular research points at present.

We know that reduce method is introduced by ES5, and the English interpretation of reduce translates to “reduce, shrink, reduce and weaken”. MDN directly states this method as follows:

The reduce method applies a function against an accumulator and each value of the array (from left-to-right) to reduce it to a single value.

Its usage syntax:

arr.reduce(callback[, initialValue])
Copy the code

Here we give a brief introduction.

  • reduceThe first parametercallbackIs the core, which “superimposes” each item of the array, and its last return value will bereduceThe final return value of the method. It contains four arguments:
    • previousValueLast timecallbackThe return value of the function
    • currentValueThe element being processed in array traversal
    • currentIndexOptional: indicatescurrentValueCorresponding index in the array. If providedinitialValue, the start index is 0; otherwise, the start index is 1
    • arrayOptional, callreduce()An array of
  • initialValueOptional, as the first callcallbackIs the first parameter of. If not providedinitialValue, then the first element in the array will becallbackThe first argument to.

reduceimplementationrunPromiseInSequence

Let’s look at a typical use of this: run promises in order:

const runPromiseInSequence = (array, value) => array.reduce(
    (promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(value)
)
Copy the code

The runPromiseInSequence method will be called by an array that returns a Promise for each item and executes each Promise in turn. If you find this confusing, here’s an example:

const f1 = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('p1 running')
        resolve(1)
    }, 1000)
})

const f2 = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('p2 running')
        resolve(2)
    }, 1000)
})


const array = [f1, f2]

const runPromiseInSequence = (array, value) => array.reduce(
    (promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(value)
)

runPromiseInSequence(array, 'init')
Copy the code

The execution result is shown as follows:

Reduce implementation pipe

Another typical use of reduce can be seen in the implementation of the functional method pipe: Pipe (f, g, h) is a curried function that returns a new function that completes (… args) => h(g(f(… Args))). That is, the function returned by the PIPE method receives an argument that is passed to the first argument of the pipe method for it to call.

const pipe = (... functions) => input => functions.reduce( (acc, fn) => fn(acc), input )Copy the code

Consider the methods runPromiseInSequence and PIPE, which are typical scenarios for Reduce applications.

To implement areduce

So how do we implement a Reduce? Refer to Polyfill from MDN:

if(! Array.prototype.reduce) { Object.defineProperty(Array.prototype,'reduce', {
    value: function(callback /*, initialValue*/) {
      if (this === null) {
        throw new TypeError( 'Array.prototype.reduce ' + 
          'called on null or undefined')}if(typeof callback ! = ='function') {
        throw new TypeError( callback +
          ' is not a function')
      }
	
      var o = Object(this)
	
      var len = o.length >>> 0
	
      var k = 0
      var value
	
      if (arguments.length >= 2) {
        value = arguments[1]
      } else {
        while(k < len && ! (kin o)) {
          k++
        }
	
        if (k >= len) {
          throw new TypeError( 'Reduce of empty array ' +
            'with no initial value' )
        }
        value = o[k++]
      }
	
      while (k < len) {
        if (k in o) {
          value = callback(value, o[k], k, o)
        }
    
        k++
      }
	
      return value
    }
  })
}
Copy the code

Value is used as the initial value in the above code, and the result of value is successively calculated and output through the while loop. However, compared with the above implementation of MDN, I prefer the following implementation scheme:

Array.prototype.reduce = Array.prototype.reduce || function(func, initialValue) {
    var arr = this
    var base = typeof initialValue === 'undefined' ? arr[0] : initialValue
    var startPoint = typeof initialValue === 'undefined' ? 1 : 0
    arr.slice(startPoint)
        .forEach(function(val, index) {
            base = func(base, val, index + startPoint, arr)
        })
    return base
}
Copy the code

The core principle is to use forEach instead of while to accumulate results, which are essentially the same.

I also have a look at Pollyfill in ES5-SHIm, which is completely consistent with the above ideas. The only difference is that I used forEach iterations while ES5-Shim uses a simple for loop. In fact, if we were more careful, we would point out that the forEach method for arrays is also a new addition to ES5. Therefore, it makes little sense to use one ES5 API (forEach) to implement another ES5 API (Reduce) — in this case pollyfill is a simulated degradation solution in an ES5-incompatible situation. I will not go into details here, because the fundamental purpose is to give readers a thorough understanding of Reduce.

Know reduce through Koa Only module source code

By understanding and implementing the Reduce approach, we already have a better understanding of it. Finally, let’s take a look at an example of reduce usage — through the ONLY module of the Koa source code to deepen the impression:

var o = {
    a: 'a',
    b: 'b',
    c: 'c'
}
only(o, ['a'.'b'])   // {a: 'a',  b: 'b'}
Copy the code

This method returns a new object with the specified filter attribute. Only module implementation:

var only = function(obj, keys){
    obj = obj || {}
    if ('string' == typeof keys) keys = keys.split(/ +/)
    return keys.reduce(function(ret, key) {
        if (null == obj[key]) return ret
        ret[key] = obj[key]
        return ret
    }, {})
}
Copy the code

There’s a lot to be said for the little Reduce and its spin-off scenarios. By analogy, active learning and application is the key to technological advancement.

Several options for the compose implementation

Functional concepts — this old concept is now “everywhere” in the front end. Many of the ideas for functions are useful, but one detail: Compose is widely used because of its clever design. For its implementation, from process-oriented to functional implementation, different styles, worth our exploration. For example, the compose method is something that interviewers often ask for in an interview.

Compose, like pipe, is composed to perform a series of tasks (methods) of an indefinite length, such as:

let funcs = [fn1, fn2, fn3, fn4]
letcomposeFunc = compose(... funcs)Copy the code

Perform:

composeFunc(args)
Copy the code

Equivalent to:

fn1(fn2(fn3(fn4(args))))
Copy the code

To summarize the key points of the compose method:

  • composeIs an array of functions and returns a function
  • composeAll arguments are functions that run from right to left, so the initial function must be placed on the right side of the argument
  • composeAfter execution, the returned function can receive arguments, which will be used as arguments of the initial function, so the arguments of the initial function are multivariate, and the result of the initial function will be used as arguments of the next function, and so on. Therefore, all functions except the initial function receive unary values.

We find that compose and Pipe actually differ only in the order in which they are called:

// compose
fn1(fn2(fn3(fn4(args))))
	
// pipe
fn4(fn3(fn2(fn1(args))))
Copy the code

As with the PIPE method we implemented earlier, what further analysis is there? Read on to see what else you can do.

Compose’s simplest implementation is procedural oriented:

const compose = function(... args) {let length = args.length
    let count = length - 1
    let result
    return functionf1 (... arg1) { result = args[count].apply(this, arg1)if (count <= 0) {
            count = length - 1
            return result
        }
        count--
        return f1.call(null, result)
    }
}
Copy the code

The key here is the use of closure, the use of closure variables to store the result and the length of the function array and traversal index, and the use of recursive thinking, the cumulative calculation of the results. The overall implementation conforms to normal process-oriented thinking and is not difficult to understand.

Smart students may also realize that using the reduce approach described above should solve the problem more functionally:

const reduceFunc = (f, g) => (... arg) => g.call(this, f.apply(this, arg)) const compose = (... args) => args.reverse().reduce(reduceFunc, args.shift())Copy the code

With the call and apply methods, this implementation is not hard to understand.

We continued thinking, “Since concatenation and flow control are involved,” we could also use Promise to implement:

const compose = (... args) => {let init = args.pop()
    return(... arg) => args.reverse().reduce((sequence, func) => sequence.then(result => func.call(null, result)) , Promise.resolve(init.apply(null, arg))) }Copy the code

This implementation takes advantage of the Promise feature: the logic is first started with promise.resolve (init.apply(null, arg)), a resolve value is started as the return value of the last function receiving arguments, and the functions are executed in turn. Because promise.then() still returns a promise type value, reduce can simply execute as a Promise instance.

If you can implement it using Promise, then generator should be implemented as well. Here is a question for you to think about. If you are interested, you can try it out and discuss it in the comments section.

Finally, let’s take a look at the well-known implementations of Lodash and Redux on the community.

Lodash version

// Lodash version var compose =function(funcs) {
    var length = funcs.length
    var index = length
    while (index--) {
        if(typeof funcs[index] ! = ='function') {
            throw new TypeError('Expected a function'); }}return function(... args) { var index = 0 var result = length ? funcs.reverse()[index].apply(this, args) : args[0]while (++index < length) {
            result = funcs[index].call(this, result)
        }
        return result
    }
}
Copy the code

The LoDash version is more like our first implementation and easier to understand.

Version of the story

/ / version of the storyfunctioncompose(... funcs) {if (funcs.length === 0) {
        return arg => arg
    }
	
    if (funcs.length === 1) {
        return funcs[0]
    }
	
    returnfuncs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code

In short, the reduce method of arrays is fully utilized.

The functional concept is a bit abstract and requires careful thinking and hands-on debugging. Once you have an Epiphany, you must feel the elegance and simplicity.

Apply, bind advanced implementation

There’s been a lot of talk about the this binding in interviews, and there’s been a lot of community discussion about how to implement the bind method. But much of the content is not systematic, and there are some flaws. Here’s a quick excerpt from an article I wrote in early 2017 that goes from an interview question to “I may have read fake source code.” In this tutorial, we introduced the implementation of bind, but here we expand further.

The use of bind will not be covered here, but you can fill in the basics for those who are not sure. Let’s start with a rudimentary implementation:

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var argsArray = Array.prototype.slice.call(arguments);
    return function () {
        return me.apply(context, argsArray.slice(1))
    }
}
Copy the code

This is the answer given by the average qualified developer, and if the candidate can write this, give him 60 points.

A quick read:

The basic principle is to simulate BIND using Apply. The this inside the function is the function that needs to bind to this, or the antifunction. Finally, use apply to bind the context and return.

At the same time, using parameters other than the first context as default parameters provided to the original function is a basic “currying” basis.

The problem with argsarray.slice (1) is that the preset parameter function is missing.

Imagine returning a binding function that faces an awkward situation if you want to implement default arguments (as bind does). The “perfect way” to truly curryize is:

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        returnme.apply(context, finalArgs); }}Copy the code

If the function returned by bind appears as a constructor with the new keyword, our bind this will need to be “ignored” and bound to the instance. That is, the new operator is higher than the bind binding, compatible with implementations of this case:

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var F = function () {};
    F.prototype = this.prototype;
    var bound = function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(this instanceof F ? this : context || this, finalArgs);
    }
    bound.prototype = new F();
    return bound;
}
Copy the code

If you think that’s it, I’ll tell you, the climax is just around the corner. I thought the above method was pretty good until I looked at the es5-SHIm source code (which has been truncated) :

function bind(that) {
    var target = this;
    if(! isCallable(target)) { throw new TypeError('Function.prototype.bind called on incompatible ' + target);
    }
    var args = array_slice.call(arguments, 1);
    var bound;
    var binder = function () {
        if (this instanceof bound) {
            var result = target.apply(
                this,
                array_concat.call(args, array_slice.call(arguments))
            );
            if ($Object(result) === result) {
                return result;
            }
            return this;
        } else {
            returntarget.apply( that, array_concat.call(args, array_slice.call(arguments)) ); }}; var boundLength = max(0, target.length - args.length); var boundArgs = [];for (var i = 0; i < boundLength; i++) {
        array_push.call(boundArgs, '$' + i);
    }
    bound = Function('binder'.'return function (' + boundArgs.join(', ') + '){ return binder.apply(this, arguments); } ')(binder);
	
    if (target.prototype) {
        Empty.prototype = target.prototype;
        bound.prototype = new Empty();
        Empty.prototype = null;
    }
    return bound;
}
Copy the code

What the hell is going on with the ES5-SHIm implementation? You may not know this, but every function has a length attribute. Yeah, just like arrays and strings. The length property of a function, which represents the number of parameters of the function. More importantly, the value of the function’s length attribute is not overridden. I wrote a test code to prove it:

function test(){} test.length // output 0 test.hasownProperty ()'length') / / outputtrue
Object.getOwnPropertyDescriptor('test'.'length'// signals: // different:false, 
// enumerable: false,
// value: 4, 
// writable: false 
Copy the code

At this point, it’s easy to explain: ** ES5-shim is for maximum compatibility, including restoring the length attribute of the return function. ** The length value is always zero if we implement it the way we did before. So, since you can’t change the value of the length attribute, you can always assign the value at initialization. We can then define functions dynamically through eval and new Function. However, for security reasons, using the eval or Function() constructors in some browsers throws an exception. Coincidentally, however, these incompatible browsers mostly implement bind, and these exceptions are not triggered. In the above code, reset the length property of the binding function:

var boundLength = max(0, target.length - args.length)
Copy the code

Constructor calls, which are also valid with Binder:

if(this instanceof bound) { ... // constructor calls}else{... // Call normal}if(target.prototype) { Empty.prototype = target.prototype; bound.prototype = new Empty(); Empty. Prototype = null; }Copy the code

Having compared several versions of polyfill implementations, you should have a good understanding of BIND. This series of implementations effectively examines the hard qualities such as the orientation of this, JavaScript closures, prototypes and prototype chains, boundary cases in design programs, and compatibility considerations.

#### A better interview question

Finally, in many interviews these days, interviewers will ask “Bind.” ** If I were you, I would probably avoid this easy “take an exam” question right now. Instead, I would be creative and ask interviewees to “call/apply”. ** We use call/apply to simulate bind, and call/apply is simple:

Function.prototype.applyFn = function (targetObject, argsArray) {
    if(typeof argsArray === 'undefined' || argsArray === null) {
        argsArray = []
    }
	
    if(typeof targetObject === 'undefined' || targetObject === null){
        targetObject = this
    }
	
    targetObject = new Object(targetObject)
	
    const targetFnKey = 'targetFnKey'targetObject[targetFnKey] = this const result = targetObject[targetFnKey](... argsArray) delete targetObject[targetFnKey]return result
}
Copy the code

This code is easy to understand; the this inside the function refers to the function that calls applyFn. To bind this inside the function to targetObject, we use an implicit binding: targetObject[targetFnKey](… ArgsArray).

Careful readers will notice that if the targetObject object already has an attribute like targetFnKey, the value of the original targetFnKey attribute will be overwritten and then deleted when applyFn is used. The solution can use ES6 Sybmol() to ensure bond uniqueness; Another solution, which we won’t go over here, is to implement unique keys with math.random ().

The implications of implementing these apis

These apis aren’t too complicated to implement, but they are a good test of a developer’s JavaScript foundation. The foundation is the foundation, the key to deeper exploration, the most important step on the road to progress, and every developer needs to pay attention to it. In today’s rapid development and iteration of front-end technology, in the “front-end market is saturated”, “front-end job search is extremely popular”, “front-end entry is simple, many people silly money” and other fickent environment, the training of basic internal skills is particularly important. It’s also key to how far and how long you can go on the front end.

From the point of view of the interview, the interview question is in the final analysis to the basis of investigation, only to be thoroughly familiar with the foundation of the chest, in order to have a breakthrough interview basic conditions.

To share

This article is a basic part of my course: Advanced Core Knowledge of Front-end Development.

Interested readers can:

Click on PC to learn more about advanced Core Knowledge of front-end Development

Mobile click to learn more:

Outline content:

Happy coding!