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
- through
getBoundingClientRect
API 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.
getBoundingClientRect
methods
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.
reduce
The first parametercallback
Is the core, which “superimposes” each item of the array, and its last return value will bereduce
The final return value of the method. It contains four arguments:previousValue
Last timecallback
The return value of the functioncurrentValue
The element being processed in array traversalcurrentIndex
Optional: indicatescurrentValue
Corresponding index in the array. If providedinitialValue
, the start index is 0; otherwise, the start index is 1array
Optional, callreduce()
An array of
initialValue
Optional, as the first callcallback
Is the first parameter of. If not providedinitialValue
, then the first element in the array will becallback
The first argument to.
reduce
implementationrunPromiseInSequence
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:
compose
Is an array of functions and returns a functioncompose
All arguments are functions that run from right to left, so the initial function must be placed on the right side of the argumentcompose
After 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!