前端大厂面试题收集(一)

165 阅读9分钟

1. JS闭包,你了解多少 知识点一: 函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被同名变量赋值后覆盖

 console.log(a) // ƒ a(){}  变量a赋值前打印的都会是函数a
 var a=1;
 function a(){}
 console.log(a) // 1    变量a赋值后打印的都会是变量a的值

2. 代码的输出结果

function Dog() {
  this.name = 'puppy'
}
Dog.prototype.bark = () => {
  console.log('woof!woof!')
}
const dog = new Dog()
console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)

结果: true;

解析: 因为constructor是prototype的属性,所以dog.constructor 实际上就是指Dog.prototype.constructor;constructor属性指向构造函数;instanceof检测的是类型是否在实例的原型链上。

constructor和instanceof的作用是不同的,constructor用来严格对比对象的构造函数是不是指定的值,而instanceof则只要检测的类型在原型链上,都会返回true。

3. 同源策略

同源策略指的是protocol(协议)、domain(域名)、port(端口)三者必须一致。

同源策略限制了从同一个源加载的文档或脚本去与另一个源的资源进行交互,这是浏览器的一个用来隔离潜在恶意文件的重要的安全机制。

同源政策主要限制了三个方面:

  • 当前域下的js脚本不能访问其他域下的cookie、localStorage 和 indexDB
  • 当前域下的js脚本不能操作访问其他域下的DOM
  • 当前域下的ajax无法发送跨域请求。

目的: 为了保证用户的信息安全。它只是对js脚本的一种限制,并不是对浏览器的限制,对于一般的img或者script脚本请求不会有跨域的限制,因为这些操作不会通过响应结果来进行可能出现安全问题的操作。

4. 图片懒加载

类似于淘宝、京东,有大量的图片,如果使所有图片一次性加载完成,那用户的体验感很差。

解决方法:懒加载,屏幕之外的图片先不加载,随着页面的滚动,当页面快进去屏幕区域时,再进行加载。

好处:1 页面加载速度快,浏览器的加载转圈很快就结束了。2 节省浏览量,不可能每位用户都会把页面从上到下滚动完。

原理:1 将img的src设置为1px* 1px的gif透明图(所有图片都用一张,只需发送一次请求);然后将background设置为一个loading图,将真正的图片路径先暂存到图片的img的自定义属性data-url中。 2监听页面的滚动事件,当元素进入视口后,替换src为真正的url地址。实现真正的按需加载。

代码实现: 监听滚动+getBoundingClientRect()

下面为getBoundingClientRect()的示意图。

image.png

<body>
    <!--图片懒加载 -->
    <img src="./img/loading.gif" data-url="./img/1.png">
    <img src="./img/loading.gif" data-url="./img/2.jpg">
    <img src="./img/loading.gif" data-url="./img/3.jpg">
    <img src="./img/loading.gif" data-url="./img/4.jpg">
    <img src="./img/loading.gif" data-url="./img/5.jpg">
    <img src="./img/loading.gif" data-url="./img/6.jpg">

    <script>

        const displayImg = (function() {
            const offsetHeight = window.innerHeight || document.documentElement.clientHeight
            let imgs = Array.from(document.getElementsByTagName('img'))

            return function() {
                console.log(imgs)
                imgs.forEach((item, index) => {
                    const oBouding = item.getBoundingClientRect()
                    if(oBouding.top <= offsetHeight) {
                        item.setAttribute('src', item.getAttribute('data-url'))
                        imgs.splice(index, 1)

                        if (imgs.length === 0) {
                            document.removeEventListener('scroll', imgThrottle)
                        }
                    }
                })
            }
        })()
        const throttle = function(fn, delay) {
            let timer = null
            return function() {
                if (!timer) {
                    timer = setTimeout(() => {
                        fn.apply(this, arguments)
                        timer = null
                        clearTimeout(timer)
                    }, delay)
                }
            }          
        }
        // 1. 一上来立即执行一次
        displayImg()
        // 2. 监听滚动事件
        imgThrottle = throttle(displayImg, 200)
        document.addEventListener('scroll', imgThrottle)
    </script>
</body>

5. settimeout 模拟实现 setinterval

        const fn = function() {
            console.log('hehehe')
        }
        const mySetInterval = function(fn, delay, times) {
            if (!times) return false
            const i= setTimeout(() => {
                fn()
                mySetInterval(fn,delay,--times)
            }, delay);
        }
        mySetInterval(fn, 2000, 3)

6 文档声明(Doctype)和<!Doctype html>有何作用? 严格模式与混杂模式如何区分?它们有何意义?

文档声明的作用: 告诉浏览器使用哪种HTML版本来显示网页

< !Doctype html >的作用: 采用html5的版本来显示网页,并让浏览器进入标准模式。如果不写,浏览器就会进入混杂模式。(ps:< !DOCTYPE >不是HTML标签 )

严格模式:又称标准模式,指浏览器按照w3c标准解析代码; 混杂模式:又称怪异模式、兼容模式,是指浏览器按照自己的标准解析代码。通常模拟老式浏览器的行为,以防老站点无法工作。

7 原型链和原型链的继承

原型链:当对象查找一个属性时,如果没有在自身找到,那么就会查找自身的原型,如果原型还没有找到,就会继续查找原型的原型,直到找打Object.prototype的原型为止,此时原型为null。 这种通过原型链接逐级向上查找的过程被称为原型链。

原型链的继承:一个对象可以使用另一个对象的属性或方法,就称为继承。具体是通过将这个对象的原型设置为另一个对象,这样根据原型链的规则,如果查找一个对象属性且在自身不存在时,就会查找另外一个对象,相当于一个对象可以使用另一个对象的属性或方法了。

8 Promise.all 和promise.race 的区别

Promise.all可以将多个promise实例包装成一个新的promise实例,成功时返回一个结果数组,数组里面的数据顺序和接收时的数据顺序是一致的,失败时返回最先被reject失败状态的值。 Promise.race里面哪个结果先返回,就返回该结果,不论成功还是失败。当要做一件事,超过多长时间就不做时,可以用该方法解决。

    function requestImg() {
            return new Promise((resolve, reject) => {
                var img = new Image()
                img.src = 'xxxxxx'
                img.onload = function () {
                    console.log('zouni----')
                    resolve(img)
                }
            })
        }
        function timeout() {
            return new Promise(function(resolve, reject) {
                setTimeout(function() {
                    reject('图片请求超时')
                    // resolve('-图片请求超时啦-')
                }, 2000)
            })
        }
        Promise.race([requestImg(), timeout()]).then(function(res) {
            console.log(res)
        }).catch(function(reason) {
            console.log(reason)
        })

9 代码输出结果

     const p = new Promise((resolve, reject) => {
            console.log(1)
            console.log(2)
        })
        p.then(() => {
            console.log(3)
        })
        console.log(4)
        
结果: 1,2,4

10 实现数组原型方法

实现forEach方法:

        const ary = [1, 2, 3, 4]
        const person = {
            name: '战哥'
        }

        ary.forEach(function (item, index, a) {
            console.log(this)
            console.log(`当前元素为${item}, 索引为${index}, 属于数组${a};${this.name}真帅`)
        }, person)

        console.log('分割线-------')

        Array.prototype.forEach1 = function (callback, thisArg) {
            let _this = window
            if (typeof callback !== 'function') {
                throw new TypeError(callback + ' is not a function')
            }
            if(arguments.length > 1) {
                _this = thisArg
            }
            for (let i = 0, len = this.length; i < len; i++) {
                callback.call(_this, this[i], i, this)
            }            
        }
        ary.forEach1(function (item, index, a) {
            console.log(this)
            console.log(`当前元素为${item}, 索引为${index}, 属于数组${a};${this.name}太帅啦`)
        }, person)

for,forEach,map的性能比较:

for > forEach > map 在chrome 62 和 Node.js v9.1.0环境下:for循环比forEach快1倍,forEachmap快20%左右。

原因分析:

for循环:没有额外的函数调用栈和上下文,所以它的实现最为简单; forEach: 函数签名中包含了参数和上下文,性能降低; map:因为会返回一个新数组,数组的创建和赋值都会需要分配内存空间,所以性能开销较大。

实现map:

        const ary = [1, 2, 3, 4]
        const obj = {
            name: '战哥'
        }
        const res = ary.map(function (item, index, ary) {
            console.log(`当前元素为${item}, 索引为${index}, 所在的数组为${ary},this为${this}, ${this.name}真帅`)
            return item * 2
        }, obj)
        console.log(res)

        Array.prototype.myMap = function(callback, thisArg) {
            if (this === null) {
                throw new TypeError('this is null or not defined')
            }
            if (typeof callback !== 'function') {
                throw new TypeError(callback + ' is not a function')
            }
            const container = []
            // console.log(this)
            for (let i = 0, len = this.length; i < len; i++) {
                const item = callback.call(thisArg, this[i], i, this)
                container.push(item) 
            }
            return container
        }
        const res2 = ary.myMap(function (item, index, ary) {
            console.log(`当前元素为${item}, 索引为${index}, 所在的数组为${ary},this为${this}, ${this.name}真帅`)
            return item * 3
        }, obj) 
        console.log(res2)

11 异步任务调度器

实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有 limit 个

       class Scheduler {
            queue = [] // 用队列保存正在执行的任务
            runCount = 0 // 正在执行的任务个数
            constructor(limit) {
                this.maxCount = limit
            }
            add(time, data) {
                const promiseCreator = () => {
                    return new Promise((resolve, reject) => {
                        setTimeout(() => {
                            console.log(data)
                            resolve()
                        }, time)
                    })
                }
                this.queue.push(promiseCreator)
                // 每次添加后尝试去执行任务
                this.request()
            }
            request() {
                if (this.queue.length && this.runCount < this.maxCount) {
                    this.runCount++
                    this.queue.shift()().then(() => {
                        this.runCount--
                        this.request()
                    })
                }
            }
        }
        const scheduler = new Scheduler(2);
        scheduler.add(1000, '1')
        scheduler.add(500, '2')
        scheduler.add(300, '3')
        scheduler.add(400, '4')
        
        // 输出结果 2314

完成下图的面试题

image.png 答案:

  class Scheduler {
            constructor(maxNum) {
                this.maxNum = maxNum
                this.taskList = []
                this.count = 0
            }
        // () => new Promise(resolve => setTimeout(resolve, time))

            async add(promiseCreator) {
                /**
                 * 此时count已经满了,不能执行本次add(包括执行),需要堵塞在这里,
                 * 所以使用promise堵塞,将resolve放在队列中等待唤醒;
                 * 等上一个执行完时,利用shift从队列中取出并执行resolve,
                 * await 执行完毕,开始往下走
                */
                if (this.count >= this.maxNum) {
                    await new Promise((resolve, reject) => {
                        this.taskList.push(resolve)
                    })
                }
                this.count++
                const res = await promiseCreator()
                this.count--
                if(this.taskList.length > 0) {
                    this.taskList.shift()()
                }
                return res
            }
        }
        const timeout = (time) => new Promise(resolve => {
            setTimeout(resolve, time)
        })

        const scheduler = new Scheduler(2);
        const addTask = (time, order) => {
            scheduler.add(() => timeout(time)).then(() => console.log(order))
        }
        addTask(1000, '1');
        addTask(500, '2');
        addTask(300, '3');
        addTask(400, '4');
        // 2314

12 判断是不是数组

方法一: Array.isArray

方法二:Object.prototype.toString.call([]) === '[object Array]'

方法三:[] instanceof Array

方法四:[].constructor === Array

13 函数柯里化

  • 概念:把接收多个参数的函数转化为接收一个单一参数的函数,并且返回接收余下的参数而且返回结果的新函数。通俗来讲:函数柯里化是一种函数的转换,将一个函数从可调用的fn(a,b,c)转化为fn(a)(b)(c)

  • 作用: 1.参数复用 2.提前返回 3.延迟计算/运行

  • 参数复用:

    使用场景:用在正则校验时,比如校验手机号

    // 普通函数的正则校验手机号
      function check(regExp, string) {
          return regExp.test(string)
      }
      console.log(check(/^1\d{10}$/, '17878787878'))
      console.log(check(/^1\d{10}$/, '1787878278'))
      console.log(check(/^1\d{10}$/, '07878787870'))
      // 柯里化写法:(简化代码书写,提高代码可读性)
      function curry(fn) {
          const judge = (...args) => {
              if (args.length === fn.length) return fn(...args)
              return (...newArgs) => judge(...args, ...newArgs)
          }
          return judge
      }
      // 使用
      const checkCurried = curry(check)
      const checkPhone = checkCurried(/^1\d{10}$/)
      console.log(checkPhone('17878787878'))
      console.log(checkPhone('07878787878'))
      console.log(checkPhone('1787787878'))
    
  • 提前返回

 //下面是兼容现代浏览器以及IE浏览器的方法,普通方法:
     const addEvent = function(el, type, fn, useCapture) {
         console.log('chufa')
         if (window.addEventListener) {
             el.addEventListener(type, function(e) {
                 fn.call(el, e)
             }, useCapture)  
         } else if (window.attachEvent) {
             el.attachEvent('on' + type, function(e) {
                 fn.call(el, e)
             })
         }
     }
     /**
     *以上写法会导致每次使用触发事件时,都会触发 if、else;如果使用
     *柯里化,则只会在第一次走if、else,之后不会再触发
     */ 
     const addEventCurried = (function() {
         console.log('chufa2')
         if (window.addEventListener) {
             return function(el, type, fn, capture) {
                 el.addEventListener(type, function(e) {
                     fn.call(el,e)
                 }, capture)
             }
         } else if (window.attachEvent) {
             return function(el, type, fn, capture) {
                 el.attachEvent('on' + type, function(e){
                     fn.call(el,e)
                 })
             }
         }
     })()   
     const btn = document.getElementById('btn')
     const btnClickFn = function(e) {
         console.log('click')
         console.log(this)
         console.log(e)
     }
     addEvent(btn, 'click', btnClickFn, false)
     addEvent(btn, 'click', btnClickFn, false)
     addEventCurried(btn, 'click', btnClickFn, false)
     addEventCurried(btn, 'click', btnClickFn, false)

// 一般情况下,函数在传入参数后,会立即执行;但有时需要先传入参数,等到某个时机再去执行,此时可以使用柯里化,将参数先放到数组中,然后一起计算。
const initFn = function() {
         let num = 0
         for(let i = 0, len = arguments.length; i < len; i++) {
             num += arguments[i]
         }
         return num
     }
     console.log(initFn(1,2,3,4,5))
     function curry(fn) {
         let ary = []
         return function() {
             if (!arguments.length) return fn(...ary)
             ary = ary.concat([].slice.call(arguments))
         }
     }
     const curriedFn = curry(initFn)
     curriedFn(1)
     curriedFn(2)
     console.log(curriedFn())

  • 经典柯里化函数
        // 普通函数
     function add(a, b, c) {
         return a + b + c
     }
     add(1,2,3)
     // 手动柯里化后的函数
     function _add(a) {
         return function(b) {
             return function(c) {
                 return a + b + c
             }
         }
     }
     const res = _add(1)(2)(3) // 6 
     // 封装一个通用柯里化转换函数,将普通函数转化为柯里化函数。
     // 原理: 用数组将参数收集起来,达到原函数形参个数则执行原函数,否则返回一个新函数继续接收新参数
     // (也就是说,返回的每个新函数只有收集参数的作用)
     function curry(fn) {
         const argsCount = fn.length
         const judge = (...args) => {
             if (args.length === argsCount) return fn(...args)
             return (...newArgs) => judge(...args, ...newArgs)
         }
         return judge
     }
     // 测试
     const addCurried = curry(add)
     console.log(addCurried(1)(2)(3)) // 6
     console.log(addCurried(1,2)(3)) // 6
     console.log(addCurried(1)(2,3)) // 6
     console.log(addCurried(1,2,3)) // 6

14 如何获得对象非原型链上的属性

hasOwnProperty

15 什么是margin折叠问题,如何解决

margin折叠:两个块级元素的上外边距重合,或上下外边距重合的情况。当重合时,外边距的计算原则如下:如果两者都是正数,或两者都是负数,取绝对值最大的那个;如果是一正一负,则两者相加。

解决方法:

如果是兄弟之间重叠:

  • 底部元素定位(absolute、fixed)或浮动
  • 底部元素变为行内块(inline-block)

如果是父子之间重叠:

  • 父元素加入overflow: hidden
  • 父元素加透明边框: border: 1px solid transparent
  • 子元素变为行内块 display: inline-block
  • 子元素定位(absolute、fixed)或浮动

16 await等的是什么

  • 如果它等的不是一个Promise对象,那await表达式的运算结果就是它等的东西
  • 如果它等的是一个Promise对象,await就会堵塞后面的代码,等着Promise的resolve,然后得到resolve的值,作为await表达式的运算结果。
  function testAsy(x) {
           return new Promise(resolve => {
               setTimeout(() => {
                   resolve(x)
               }, 3000)
           })
       }
       async function testAwt(){    
           let result =  await testAsy('hello world');
           console.log(result);    // 3秒钟之后出现hello world
           console.log('cuger')   // 3秒钟之后出现cug
       }
       testAwt();
       console.log('cug')  //立即输出cug

参考文章:blog.csdn.net/m0_67614517…