推广 热搜: csgo  vue  angelababy  2023  gps  新车  htc  落地  app  p2p 

操作DOM的方式今天记,明天忘,真让人奔溃!

   2023-06-07 网络整理佚名1400
核心提示:上述代码并不难理解,使用递归实现。,则不计入计算,进入下一个节点(其父节点)的递归。数组方法非常重要:因为数组就是数据,数据就是状态,状态反应着视图。和函数数组长度以及遍历索引,并利用递归思想,进行结果的累加计算。值为最后一个函数接收参数后的返回值,依次执行函数。.slice(1),它的问题在于存在预置参数功能丢失的现象。属性,用于表示函数的形参个数。的属性值,那么在初始化时赋值总可以吧!

很多刚入行的同学对我说:“很多API记不住怎么办?数组这个方法那个方法总是傻傻的不清楚,怎么办?操作DOM的方法今天记住了,明天就忘记了,好郁闷啊!”

甚至有开发者在讨论面试时向我抱怨:“面试官总是执着于API的使用,甚至某些方法的参数顺序都要跟我说清楚!”

我觉得对于重复使用的方法,大家一定要做到“机械记忆”,能够反手写出来。 一些似乎永远不会被记住的API,根本就没有被充分利用过。

我当面试官的时候,从不强迫开发人员准确地“背诵”API。 相反,我喜欢换个角度审视面试官:“既然记不住怎么用,那我就告诉你怎么用,你自己去实现吧!” 实现一个API,除了考察面试官对API的理解程度之外,更能体现开发者的编程思维和编码能力。 对于有上进心的前端工程师来说,模仿和实现一些经典的方法应该是“常做的”,这是一个比较基本的要求。

在本节中,我根据自己理解的面试题和作为面试官的经验,选取了几个典型的API,通过不同程度、不同方式的实现,涵盖了API中的一些知识点和编程要点。 通过学习本节的内容,希望你不仅能了解代码的奥秘,还能学会以一例举其他情况的方法。

API专题相关知识点如下:

完成

这个话题是从今天头条某部门的一道面试题演变而来的。 当时面试官问:“如何获取文档中任意元素到文档顶部的距离?”

熟悉的同学应该不陌生,该方法返回或设置匹配元素相对于文档的偏移量(位置)。 此方法返回的对象包含两个整数属性:top 和 left,以像素为单位。 如果可用,我们可以直接调用 API 获取结果。 但是您如何以本地方式(即手动方式)进行操作呢?

有两个主要想法:

递归实现

我们通过遍历目标元素、目标元素的父节点、父节点的父节点依次溯源,累加这些遍历的节点相对于它们最近的祖先节点的偏移量(属性不是),向上 直到,相加得到结果。

其中,我们需要使用 来访问DOM节点的上边框相对于其值不是的最近的祖先元素的垂直偏移量。 具体实现是:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. const offset = ele => {

  2. let result = {

  3. top: 0,

  4. left: 0

  5. }


  6. // 当前 DOM 节点的 display === 'none' 时, 直接返回 {top: 0, left: 0}

  7. if (window.getComputedStyle(ele)['display'] === 'none') {

  8. return result

  9. }


  10. let position


  11. const getOffset = (node, init) => {

  12. if (node.nodeType !== 1) {

  13. return

  14. }


  15. position = window.getComputedStyle(node)['position']


  16. if (typeof(init) === 'undefined' && position === 'static') {

  17. getOffset(node.parentNode)

  18. return

  19. }


  20. result.top = node.offsetTop + result.top - node.scrollTop

  21. result.left = node.offsetLeft + result.left - node.scrollLeft


  22. if (position === 'fixed') {

  23. return

  24. }


  25. getOffset(node.parentNode)

  26. }


  27. getOffset(ele, true)


  28. return result

  29. }

上面的代码不难理解,是用递归实现的。 如果节点节点。 类型不是(1),跳出; 如果相关节点的属性是,则不计入计算,进入下一个节点(其父节点)的递归。 如果相关属性的属性为none,则直接返回0作为结果。

这个实现很好地考验了开发者对递归的初级应用和方法的掌握程度。

接下来我们换个思路,用一个比较新的API:t来实现方法。

方法

t方法用于描述元素的具体位置,这个位置的以下四个属性都是相对于视口左上角的位置。 在一个节点上执行该方法,返回值是一个类型的对象。 该对象表示一个矩形框,其中包含:left、top、right 等只读属性。

请参考实现:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. const offset = ele => {

  2. let result = {

  3. top: 0,

  4. left: 0

  5. }

  6. // 当前为 IE11 以下,直接返回 {top: 0, left: 0}

  7. if (!ele.getClientRects().length) {

  8. return result

  9. }


  10. // 当前 DOM 节点的 display === 'none' 时,直接返回 {top: 0, left: 0}

  11. if (window.getComputedStyle(ele)['display'] === 'none') {

  12. return result

  13. }


  14. result = ele.getBoundingClientRect()

  15. var docElement = ele.ownerdocument.documentElement


  16. return {

  17. top: result.top + window.pageYOffset - docElement.clientTop,

  18. left: result.left + window.pageXOffset - docElement.clientLeft

  19. }

  20. }

需要注意的细节是:

从这个问题可以看出,这样的实现比考察“死记硬背”的API更有意义。 我经常站在面试官的角度,给面试官(开发者)提供相关的方法提示,引导他给出最终的解决方案。

数组方法的相关实现

数组方法很重要:因为数组就是数据,数据就是状态,状态反映视图。 对数组的操作我们不能陌生,方法也应该不陌生。 我觉得这种方法很好的体现了“函数式”的概念,也是目前非常热门的研究点之一。

我们知道该方法是ES5引入的,英文解释翻译为“, , , and ”。 MDN直接将方法描述为:

a 和数组的每个值(从左到右)给它一个值。

它的使用语法是:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. arr.reduce(callback[, initialValue])

下面我们简单介绍一下。

完成

我们来看一个典型的应用:按顺序运行:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. const runPromiseInSequence = (array, value) => array.reduce(

  2. (promiseChain, currentFunction) => promiseChain.then(currentFunction),

  3. Promise.resolve(value)

  4. )

该方法会被一个数组调用,每一项返回一个,数组中的每一项都会依次执行,请仔细理解。 如果觉得晦涩难懂,可以参考例子:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. const f1 = () => new Promise((resolve, reject) => {

  2. setTimeout(() => {

  3. console.log('p1 running')

  4. resolve(1)

  5. }, 1000)

  6. })


  7. const f2 = () => new Promise((resolve, reject) => {

  8. setTimeout(() => {

  9. console.log('p2 running')

  10. resolve(2)

  11. }, 1000)

  12. })



  13. const array = [f1, f2]


  14. const runPromiseInSequence = (array, value) => array.reduce(

  15. (promiseChain, currentFunction) => promiseChain.then(currentFunction),

  16. Promise.resolve(value)

  17. )


  18. runPromiseInSequence(array, 'init')

执行结果如下:

实现管道

另一个典型的应用可以参考函数式方法pipe的实现:pipe(f,g,h)是一个柯里化函数,返回一个新的函数,完成(...args)=> h(g(f(. ..args))) 调用。 即管道方法返回的函数会接收一个参数,该参数传递给管道方法的第一个参数,供其调用。

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. const pipe = (...functions) => input => functions.reduce(

  2. (acc, fn) => fn(acc),

  3. input

  4. )

仔细理解pipe和pipe这两个方法,都是典型的应用场景。

实现一个

那么我们如何实施呢? 来自MDN的参考:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. if (!Array.prototype.reduce) {

  2. Object.defineProperty(Array.prototype, 'reduce', {

  3. value: function(callback ) {

  4. if (this === null) {

  5. throw new TypeError( 'Array.prototype.reduce ' +

  6. 'called on null or undefined' )

  7. }

  8. if (typeof callback !== 'function') {

  9. throw new TypeError( callback +

  10. ' is not a function')

  11. }


  12. var o = Object(this)


  13. var len = o.length >>> 0


  14. var k = 0

  15. var value


  16. if (arguments.length >= 2) {

  17. value = arguments[1]

  18. } else {

  19. while (k < len && !(k in o)) {

  20. k++

  21. }


  22. if (k >= len) {

  23. throw new TypeError( 'Reduce of empty array ' +

  24. 'with no initial value' )

  25. }

  26. value = o[k++]

  27. }


  28. while (k < len) {

  29. if (k in o) {

  30. value = callback(value, o[k], k, o)

  31. }


  32. k++

  33. }


  34. return value

  35. }

  36. })

  37. }

上面代码中以value作为初值,通过while循环依次累加计算value的结果并输出。 但是相比上面MDN上的实现方式,我个人比较喜欢的实现方式是:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. Array.prototype.reduce = Array.prototype.reduce || function(func, initialValue) {

  2. var arr = this

  3. var base = typeof initialValue === 'undefined' ? arr[0] : initialValue

  4. var startPoint = typeof initialValue === 'undefined' ? 1 : 0

  5. arr.slice(startPoint)

  6. .forEach(function(val, index) {

  7. base = func(base, val, index + startPoint, arr)

  8. })

  9. return base

  10. }

核心原理是用while代替while来实现结果累加,本质上是一样的。

我也看了ES5-shim中的那个,和上面的思路一模一样。 唯一的区别是:我使用的是迭代,而 ES5-shim 使用的是简单的 for 循环。 事实上,如果我们更精确一点,我们会指出数组方法对于 ES5 也是新的。 因此,用一个ES5 API( )实现另一个ES5 API( )没有实际意义——这里是模拟ES5不兼容情况下的降级方案。 这里不多做考察,根本目的是希望读者对本书有一个全面、透彻的了解。

通过koa only模块源码

通过对方法的理解和实现,我们对它有了更深的理解。 最后再来看一个使用示例——通过Koa源码的唯一模块来加深一下印象:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. var o = {

  2. a: 'a',

  3. b: 'b',

  4. c: 'c'

  5. }

  6. only(o, ['a','b']) // {a: 'a', b: 'b'}

此方法返回具有指定过滤器属性的新对象。

唯一的模块实现:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. var only = function(obj, keys){

  2. obj = obj || {}

  3. if ('string' == typeof keys) keys = keys.split(/ +/)

  4. return keys.reduce(function(ret, key) {

  5. if (null == obj[key]) return ret

  6. ret[key] = obj[key]

  7. return ret

  8. }, {})

  9. }

小场景和衍生场景中有很多值得我们深思和探索的地方。 以此类推,灵活学习和应用是技术进步的关键。

几种方案实现

函数式概念——这个古老的概念如今在前端领域“遍地开花”。 函数式风格的很多思想值得借鉴,其中一个细节:因其设计巧妙而被广泛使用。 对于它的实现,从面向过程的到函数式的,风格迥异,值得探讨。 面试的时候,面试官经常会问到实现方法。 让我们先看看它是什么。

其实就像上面说的管道一样,就是执行一系列变长的任务(方法),比如:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. let funcs = [fn1, fn2, fn3, fn4]

  2. let composeFunc = compose(...funcs)

实施:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. composeFunc(args)

相当于:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. fn1(fn2(fn3(fn4(args))))

总结该方法的要点:

我们发现,其实和pipe的区别只在于调用的顺序:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. // compose

  2. fn1(fn2(fn3(fn4(args))))


  3. // pipe

  4. fn4(fn3(fn2(fn1(args))))

既然和我们前面实现的pipe方法一模一样,那还有什么值得深入分析的呢? 继续阅读,看看还有什么可以玩的。

最简单的实现是面向过程的:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. const compose = function(...args) {

  2. let length = args.length

  3. let count = length - 1

  4. let result

  5. return function f1 (...arg1) {

  6. result = args[count].apply(this, arg1)

  7. if (count <= 0) {

  8. count = length - 1

  9. return result

  10. }

  11. count--

  12. return f1.call(null, result)

  13. }

  14. }

这里的关键是使用闭包,使用闭包变量存储结果和函数数组的长度并遍历索引,使用递归的思想对结果进行累加计算。 整体实现符合正常的流程化思维,不难理解。

聪明的同学可能也意识到,使用上面提到的方法,应该可以更函数式地解决问题:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. const reduceFunc = (f, g) => (...arg) => g.call(this, f.apply(this, arg))

  2. const compose = (...args) => args.reverse().reduce(reduceFunc, args.shift())

通过前面的学习,结合call和apply方法,这样的实现不难理解。

我们继续展开思路,“既然涉及到级联和流控”,那我们也可以使用实现:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. const compose = (...args) => {

  2. let init = args.pop()

  3. return (...arg) =>

  4. args.reverse().reduce((sequence, func) =>

  5. sequence.then(result => func.call(null, result))

  6. , Promise.resolve(init.apply(null, arg)))

  7. }

这个实现利用了一个特点:先通过.(init.apply(null,arg))启动逻辑,启动一个值,就是上一个函数接收参数后的返回值,依次执行函数。 因为.then()返回的还是一个类型值,所以可以根据实例执行。

既然能实现,那当然应该是可以的。 这里有一个问题供大家思考。 有兴趣的同学可以试试。 欢迎在评论区讨论。

最后再看看社区中比较知名的和Redux的实现。

版本

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. // lodash 版本

  2. var compose = function(funcs) {

  3. var length = funcs.length

  4. var index = length

  5. while (index--) {

  6. if (typeof funcs[index] !== 'function') {

  7. throw new TypeError('Expected a function');

  8. }

  9. }

  10. return function(...args) {

  11. var index = 0

  12. var result = length ? funcs.reverse()[index].apply(this, args) : args[0]

  13. while (++index < length) {

  14. result = funcs[index].call(this, result)

  15. }

  16. return result

  17. }

  18. }

更像是我们的第一个实现,更容易理解。

Redux 版本

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. // Redux 版本

  2. function compose(...funcs) {

  3. if (funcs.length === 0) {

  4. return arg => arg

  5. }


  6. if (funcs.length === 1) {

  7. return funcs[0]

  8. }


  9. return funcs.reduce((a, b) => (...args) => a(b(...args)))

  10. }

总之,还是充分利用数组的方法。

功能概念确实有些抽象,需要开发者仔细思考,手工调试。 一旦有所顿悟,一定会感受到其中的优雅与质朴。

应用、绑定高级实现

采访中这个绑定相关的话题现在“泛滥”,社区中也有关于绑定方法实现的相关讨论。 但很多内容还不系统,存在一些瑕疵。 这里简单摘录我2017年初写的一篇文章,从一道面试题到“我可能读过假源代码”循序渐进地讨论。 在《Catch this in One Catch》一课中,我们介绍了bind的实现,这里更进一步。

bind函数的使用这里不再赘述,不清楚的读者可以自行补充基础知识。 先来看一个初级实现版本:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. Function.prototype.bind = Function.prototype.bind || function (context) {

  2. var me = this;

  3. var argsArray = Array.prototype.slice.call(arguments);

  4. return function () {

  5. return me.apply(context, argsArray.slice(1))

  6. }

  7. }

这是一般合格的开发人员提供的答案。 如果面试官能写到这里,就给他60分。

我们先简单读一下:

原理是使用apply来模拟bind。 函数体中的this就是需要绑定this的函数,或者说是原函数。 最后使用apply绑定参数()并返回。

同时,使用第一个参数( )以外的参数作为预设参数提供给原函数也是“柯里化”的基本依据。

在上面的实现方法中,我们返回的参数列表包括:.slice(1),它的问题是失去了预设参数的功能。

想象一下,在我们返回的绑定函数中,如果要实现预置参数传递(就像bind做的那样),我们会面临一个尴尬的局面。 真正“咖喱”的“完美方式”是:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. Function.prototype.bind = Function.prototype.bind || function (context) {

  2. var me = this;

  3. var args = Array.prototype.slice.call(arguments, 1);

  4. return function () {

  5. var innerArgs = Array.prototype.slice.call(arguments);

  6. var finalArgs = args.concat(innerArgs);

  7. return me.apply(context, finalArgs);

  8. }

  9. }

但是继续探索,我们要注意bind方法:如果bind返回的函数作为构造函数,并且以new关键字出现,那么我们的绑定this就需要“忽略”,必须将this绑定到实例上。 也就是说new操作符高于bind绑定,兼容这种情况的实现:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. Function.prototype.bind = Function.prototype.bind || function (context) {

  2. var me = this;

  3. var args = Array.prototype.slice.call(arguments, 1);

  4. var F = function () {};

  5. F.prototype = this.prototype;

  6. var bound = function () {

  7. var innerArgs = Array.prototype.slice.call(arguments);

  8. var finalArgs = args.concat(innerArgs);

  9. return me.apply(this instanceof F ? this : context || this, finalArgs);

  10. }

  11. bound.prototype = new F();

  12. return bound;

  13. }

如果你认为这是结束,我会告诉你,高潮才刚刚开始。 之前一直认为上面的方法很完美,直到看了es5-shim的源码(已适当删除):

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. function bind(that) {

  2. var target = this;

  3. if (!isCallable(target)) {

  4. throw new TypeError('Function.prototype.bind called on incompatible ' + target);

  5. }

  6. var args = array_slice.call(arguments, 1);

  7. var bound;

  8. var binder = function () {

  9. if (this instanceof bound) {

  10. var result = target.apply(

  11. this,

  12. array_concat.call(args, array_slice.call(arguments))

  13. );

  14. if ($Object(result) === result) {

  15. return result;

  16. }

  17. return this;

  18. } else {

  19. return target.apply(

  20. that,

  21. array_concat.call(args, array_slice.call(arguments))

  22. );

  23. }

  24. };

  25. var boundLength = max(0, target.length - args.length);

  26. var boundArgs = [];

  27. for (var i = 0; i < boundLength; i++) {

  28. array_push.call(boundArgs, '$' + i);

  29. }

  30. bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder);


  31. if (target.prototype) {

  32. Empty.prototype = target.prototype;

  33. bound.prototype = new Empty();

  34. Empty.prototype = null;

  35. }

  36. return bound;

  37. }

es5-shim 实现到底在做什么? 您可能不知道,但每个函数都有属性。 是的,就像数组和字符串一样。 函数的属性,用来表示函数的形参个数。 更重要的是,函数的属性值不能被覆盖。 我写了一个测试代码来演示:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. function test (){}

  2. test.length // 输出 0

  3. test.hasOwnProperty('length') // 输出 true

  4. Object.getOwnPropertyDescriptor('test', 'length')

  5. // 输出:

  6. // configurable: false,

  7. // enumerable: false,

  8. // value: 4,

  9. // writable: false

说到这里,很好解释:es5-shim是为了最大限度的兼容,包括返回函数属性的恢复。 而在我们之前实现它的方式中,该值始终为零。 所以,既然属性值不能被修改,那么在初始化的时候总是可以赋值的! 所以我们可以通过eval和sum来动态定义函数。 但出于安全原因,使用 eval 或 () 构造函数在某些浏览器中会抛出异常。 不过巧合的是,这些不兼容的浏览器基本都实现了bind功能,不会触发这些异常。 在上面的代码中,重置绑定函数的属性:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. var boundLength = max(0, target.length - args.length)

构造函数调用案例也有效兼容:

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. if (this instanceof bound) {

  2. ... // 构造函数调用情况

  3. } else {

  4. ... // 正常方式调用

  5. }


  6. if (target.prototype) {

  7. Empty.prototype = target.prototype;

  8. bound.prototype = new Empty();

  9. // 进行垃圾回收清理

  10. Empty.prototype = null;

  11. }

  12. ```


  13. 对比过几版的 polyfill 实现,对于 `bind` 应该有了比较深刻的认识。这一系列实现有效地考察了很重要的知识点:比如 `this` 的指向、Javascript 闭包、原型与原型链,设计程序上的边界 case 和兼容性考虑经验等硬素质。


  14. ####一道更好的面试题


  15. 最后,现如今在很多面试中,面试官都会以“实现 `bind`”作为题目。**如果是我,现在可能会规避这个很容易“应试”的题目,而是别出心裁,让面试者实现一个 “call/apply”。**我们往往用 `call`/`apply` 模拟实现 `bind`,而直接实现 `call`/`apply` 也算简单:

.. = (, ) {if( === '' || === null) { = []}

portant;border-width: 1px !important;border-style: solid !important;border-color: rgb(226, 226, 226) !important;">
  1. if(typeof targetObject === 'undefined' || targetObject === null){

  2. targetObject = this

  3. }


  4. targetObject = new Object(targetObject)


  5. const targetFnKey = 'targetFnKey'

  6. targetObject[targetFnKey] = this


  7. const result = targetObject[targetFnKey](...argsArray)

  8. delete targetObject[targetFnKey]

  9. return result

}```

这样的代码不难理解,函数体中的this指向被调用的函数。 为了在函数体中绑定 this,我们使用隐式绑定方法:[](...)。

细心的读者会发现这里有个问题:如果对象本身有这样一个属性,那么在使用该函数的时候,原来的属性值会被覆盖,然后删除。 解决方案可以使用()来保证key的唯一性; 另一种方案是使用Math.()实现唯一键,这里不再赘述。

实施这些 API 的影响

这些API的实现并不复杂,但是很能考验开发者的基础。 基础是基础,是探索更深入内容的关键,也是进阶道路上最重要的一环,需要每一位开发者的重视。 在前端技术快速发展迭代的今天,在浮躁的环境下,“前端市场是否饱和”、“前端求职火爆”、“前端容易上手,很多人都是花钱傻”,基本内功的修养尤为重要。 . 这也是你在前端这条路上能走多远、能走多远的关键。

从面试的角度来看,面试题归根结底是对基础的考察。 只有熟悉了基础知识,才能具备突破面试的基本条件。

 
反对 0举报 0 收藏 0 打赏 0评论 0
 
更多>同类资讯
推荐图文
推荐资讯
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报
Powered By DESTOON