美文网首页
常见面试题总结(一)

常见面试题总结(一)

作者: 菩灵 | 来源:发表于2021-01-28 21:20 被阅读0次

    常见面试题总结(一)

    什么是原型?

    • 原型分为两种:构造函数原型,称为prototype;实例(对象)原型称为proto

    原型之间的关系?

    • 由构造函数创建出来的实例对象上,proto属性指向了构造函数的prototype属性

    构造函数和构造器是什么?

    • js中,默认每一个(正常定义的)函数,都是构造器,也称为类,那么构造函数上的原型链上有ƒ Function() { [native code] }这个属性,所以可以通过new 方法创建实例,构造函数原型prototype上有一个constructor构造器属性,指向了构造函数自身
    • 能够称为构造器的关键在于拥有--->prototype属性(实例没有)
    function Fa() {}
    
    Fa.constructor
    ƒ Function() { [native code] }
    
    • 被new出来的实例,由于不是一个构造器,所以无法通过new创建新的实例--->没有prototype属性
    const son = new Fa()
    
    son.constructor
    // ƒ Fa() {}
    const subSon = new son()
    // VM5278:1 Uncaught TypeError: son is not a constructor
        at <anonymous>:1:16
    
    • 一张图描述清楚构造器想关
    • image.pngimage.png
    • 有些人可能会把 __proto__prototype 搞混淆。从翻译的角度来说,它们都可以叫原型,但是其实是完全不同的两个东西。

    __proto__ 存在于所有的对象上,prototype 存在于所有的函数上,他俩的关系就是:函数的 prototype 是所有使用 new 这个函数构造的实例的 __proto__。函数也是对象,所以函数同时有 __proto__prototype

    作者:余腾靖
    链接:https://juejin.im/post/5e2ff7dce51d4558021a1a4d
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    原型链是什么?

    • 实例拥有的实例原型对象proto属性,指向了构造函数原型对象prototype属性,而构造函数原型对象prototype上的proto属性也指向了父构造函数的原型prototype,以此类推,通过proto建立起来的指向关系就被称为原型链

    原型链的作用?

    • 实例访问一个属性的时候,先在自身可遍历属性上寻找,再到原型链上逐层向上寻找,如果找不到会返回undefined
    • stu.proto.proto.proto,直到遇到null,则返回undefined
    • image.pngimage.png
    • 作用:将类的方法定义在原型上,通过这种方式,所有的实例都可以访问到这个方法,并且这个方法只需要占用一份内存,节省内存,this 的指向还能正确指向类的实例。
    • 注意:这种方法定义的属性,由于不是自身属性,所以无法通过Object.keys()枚举
    function Engineer(workingYears) {
      this.workingYears = workingYears;
    }
    
    // 不能使用箭头函数,箭头函数的 this 在声明的时候就根据上下文确定了
    Engineer.prototype.built = function() {
      // this 这里就是执行函数调用者
      console.log(`我已经工作 ${this.workingYears} 年了, 我的工作是拧螺丝...`);
    };
    
    const engineer = new Engineer(5);
    // this 会正确指向实例,所以 this.workingYears 是 5
    engineer.built(); // => 我已经工作 5 年了, 我的工作是拧螺丝...
    console.log(Object.keys(engineer)); // => [ 'workingYears' ]
    
    
    • 优点:通过这种方式,所有的实例都可以访问到这个方法,并且这个方法只需要占用一份内存,节省内存,this 的指向还能正确指向类的实例。
    • 不可枚举示例:
    function Engineer(workingYears) {
      this.workingYears = workingYears;
    }
    
    // 不能使用箭头函数,箭头函数的 this 在声明的时候就根据上下文确定了
    Engineer.prototype.built = function() {
      // this 这里就是执行函数调用者
      console.log(`我已经工作 ${this.workingYears} 年了, 我的工作是拧螺丝...`);
    };
    
    const engineer = new Engineer(5);
    // this 会正确指向实例,所以 this.workingYears 是 5
    engineer.built(); // => 我已经工作 5 年了, 我的工作是拧螺丝...
    console.log(Object.keys(engineer)); // => [ 'workingYears' ]
    
    
    • 可枚举方法示例:
    function Engineer(workingYears) {
      this.workingYears = workingYears;
      this.built = function() {
        console.log(`我已经工作 ${this.workingYears} 年了, 我的工作是拧螺丝...`);
      };
    }
    
    const engineer = new Engineer(5);
    console.log(Object.keys(engineer)); // => [ 'workingYears', 'built' ]
    
    • 常见:Array.prototype.slice,Object.prototype.toString就是定义在了原型上的函数。

    <br />

    ES6的class本质是什么?

    • 其实,ES6 class 就是构造器的语法糖
    • ES6 的 class 就是构造器,class 上的方法定义在构造器的 prototype 上,因此你也可以理解为什么 class 的方法是不可枚举的。

    <br />

    class extends实现继承的本质?

    • 原型继承+组合继承
    // 原型继承
    function _inheritsLoose(subClass, superClass) {
      subClass.prototype = Object.create(superClass.prototype);
      subClass.prototype.constructor = subClass;
      // 让子类可以访问父类上的静态属性,其实就是定义在构造器自身上的属性
      // 例如父类有 Person.say 属性,子类 Student 通过可以通过 Student.say 访问
      subClass.__proto__ = superClass;
    }
    
    var Shape = function Shape(x, y) {
      this.x = x;
      this.y = y;
    };
    
    var Circle = (function(_Shape) {
      _inheritsLoose(Circle, _Shape);
    
      function Circle(x, y, r) {
        var _this;
    
        // 组合继承
        _this = _Shape.call(this, x, y) || this;
        _this.r = r;
        return _this;
      }
    
      var _proto = Circle.prototype;
    
      _proto.draw = function draw() {
        console.log(
          '\u753B\u4E2A\u5750\u6807\u4E3A (' +
            this.x +
            ', ' +
            this.y +
            ')\uFF0C\u534A\u5F84\u4E3A ' +
            this.r +
            ' \u7684\u5706'
        );
      };
    
      return Circle;
    })(Shape);
    
    

    继承有哪几种方式?

    (1)借助构造函数+call/apply实现继承

    // 借助构造函数实现继承
    function Parent(argument) {
        this.name='parent';
    }
    Parent.prototype.say=function(){}
    function Child(argument) {
        Parent.call(this); // 原理就是call/apply, call和apply改变了父类this中的指向,使this指向了子类,这样就可以把父类的属性挂载到子类里
        this.age=11;
    }
    var child = new Child();
    console.log(child);  // Child {name: "parent", age: 11}
    

    缺点是只能继承父类实例上的属性,无法继承原型链上的属性。<br />(2)借助原型链实现继承

    // 借助原型链实现继承
    function Parent1(argument) {
        this.name='parent';
        this.age=[1,2,3];
    }
    function Child1(argument) {
        this.name='child';
    }
    Child1.prototype=new Parent1(); // 将父类的实例赋值給子类的原型,这样子类就继承了父类
    var child11 = new Child1();
    console.log(child11)  // Child1 {age: 11 , __proto__: Parent1}
    /*******************原型链继承的缺点********************/
    var child12=new Child1();
    child11.age.push(4); // 往其中一个实例的引用属性添加一个元素
    console.log(child11.age,child12.age) // 会发现都是打印出  [1, 2, 3, 4]
    

    缺点:当父类有引用属性时,由于原型对象的特点,多个实例对象的proto都是同一个,而引用属性在new的时候不会开辟新的地址,所以当一个实例对象改变了引用属性的值时,另一个对象也会随之改变。<br />(3)结合构造函数和原型链的方式

    // 组合方式,结合上面两种
    function Parent3(argument) {
        this.name='parent';
        this.age=[1,2,3]
    }
    Parent3.prototype.say=function(){}
    function Child3(argument) {
        Parent3.call(this); // 结合构造函数
        this.type='test';
    }
    Child3.prototype=new Parent3();  // 结合原型链
    var child31 = new Child3();
    var child32 = new Child3();
    console.log(child31,child32);
    child31.age.push(4);
    console.log(child31.age,child32.age);.//  [1, 2, 3, 4]      [1, 2, 3]
    

    这种方式可以解决前两种方式的问题。缺点:父类的构造函数会执行两次。<br />优化:把上面的

    Child3.prototype=new Parent3();
    

    换成

    Child3.prototype=Parent3.prototype;
    

    以上方法还是存在不足,因为只要是通过原型链继承来的对象,它的constructor打印出来都是父类Parent3,即无法确认child31实例是由父类创造的还是由子类创造的。 原因:Child3和父类Parent3共用了一个原型。Child本身没有constructor,由于继承了父类,就会把父类的constructor作为自己的。<br />解决方案:把上面的

    Child3.prototype=Parent3.prototype;
    

    换成

    Child3.prototype=Object.create(Parent3.prototype); // 让Child3继承Parent3,由于Object.create会返回一个新对象,该对象继承了Parent3,再让Child3去继承Parent3,这样就起到了隔离并且继承的作用。
    Child3.prototype.constructor=Child3;
    // 修改Child3的constructor
    

    这样的话就是组合继承+原型继承的完美写法了。

    // 组合方式,完美写法
    function Parent3(argument) {
        this.name='parent';
        this.age=[1,2,3];
    }
    Parent3.prototype.say=function(){}
    function Child3(argument) {
        Parent3.call(this); // 结合构造函数
        this.type='test';
    }
    Child3.prototype=Object.create(Parent3.prototype);
    Child3.prototype.constructor=Child3; // 结合原型链
    var child31 = new Child3();
    var child32 = new Child3();
    console.log(child31,child32);
    child31.age.push(4);
    console.log(child31.age,child32.age);.//  [1, 2, 3, 4]      [1, 2, 3]
    

    <br />

    Object.create是做什么的?

    • Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto
    const person = {
      isHuman: false,
      printIntroduction: function () {
        console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
      }
    };
    
    const me = Object.create(person);
    
    me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
    me.isHuman = true; // inherited properties can be overwritten
    
    me.printIntroduction();
    // expected output: "My name is Matthew. Am I human? true"
    me.__proto__===person;  // true
    
    

    一道最近校招面试碰到的和原型相关的面试题

    最近面试某大厂碰到下面这道面试题:

    function Page() {
      return this.hosts;
    }
    Page.hosts = ['h1'];
    Page.prototype.hosts = ['h2'];
    const p1 = new Page();
    const p2 = Page();
    console.log(p1.hosts);
    console.log(p2.hosts);
    复制代码
    

    运行结果是:先输出 undefiend,然后报错 TypeError: Cannot read property 'hosts' of undefined。<br />为什么 console.log(p1.hosts) 是输出 undefiend 呢,前面我们提过 new 的时候如果 return 了对象,会直接拿这个对象作为 new 的结果,因此,p1 应该是 this.hosts 的结果,而在 new Page() 的时候,this 是一个以 Page.prototype 为原型的 target 对象,所以这里 this.hosts 可以访问到 Page.prototype.hosts 也就是 ['h2']。这样 p1 就是等于 ['h2']['h2'] 没有 hosts 属性所以返回 undefined。<br />为什么 console.log(p2.hosts) 会报错呢,p2 是直接调用 Page 构造函数的结果,直接调用 page 函数,这个时候 this 指向全局对象,全局对象并没 hosts 属性,因此返回 undefined,往 undefined 上访问 hosts 当然报错。<br />
    <br />作者:余腾靖<br />链接:https://juejin.im/post/5e2ff7dce51d4558021a1a4d<br />来源:掘金<br />著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。<br />

    proxy是什么?

    proxy表单校验实例

    proxy实现观察者模式

    
    const observerQueue = new Set()
    
    const observe = fn => observerQueue.add(fn)
    
    const observable = obj => new Proxy(obj, 
        set(tgt, key, val, receiver) {
            const result = Reflect.set(tgt, key, val, recevier)
            observerQueue.forEach(v => v())
            return result
        }
    )
    
    const person = observerable({age: 25, name: 'Mike'})
    const print = () => console.log(`${person.name} is ${person.age} years old`)
    observe(print)
    
    person.name = 'LiHua'
    // Lihua is 25 years old
    person.age = 45
    // Lihua is 45 years old
    

    Common.js和ES6的module比较

    • CommonJS 输出是值的拷贝,即原来模块中的值改变不会影响已经加载的该值,ES6静态分析,动态引用,输出的是值的引用,值改变,引用也改变,即原来模块中的值改变则该加载的值也改变。
    • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    • CommonJS 加载的是整个模块,即将所有的接口全部加载进来,ES6 可以单独加载其中的某个接口(方法),
    • CommonJS this 指向当前模块,ES6 this 指向undefined
    • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

    ————————————————<br />版权声明:本文为CSDN博主「冰雪为融」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。<br />原文链接:https://blog.csdn.net/lhjuejiang/article/details/80274212<br />

    yield传值与不传值的区别是什么?

    • yield每次调用next的时候传值,将作为上一次yield语句后的返回值
    • 如果不传值则值为undefined
    // 传值
    function *foo(x) {
      let y = 2 * (yield (x + 1))
      let z = yield (y / 3)
      return (x + y + z)
    }
    let it = foo(5)
    console.log(it.next())   // => {value: 6, done: false}
    console.log(it.next(12)) // => {value: 8, done: false}
    console.log(it.next(13)) // => {value: 42, done: true}
    
    // 不传值
    function *foo(x) {
      let y = 2 * (yield (x + 1))
      let z = yield (y / 3)
      return (x + y + z)
    }
    let it = foo(5)
    console.log(it.next())   // => {value: 6, done: false}
    console.log(it.next()) // => {value: 8, done: false}
    console.log(it.next()) // => {value: 42, done: true}
    VM5628:7 {value: 6, done: false}
    VM5628:8 {value: NaN, done: false}
    VM5628:9 {value: NaN, done: true}
    

    yield常见用途

    function *fetch() {
        yield ajax(url, () => {})
        yield ajax(url1, () => {})
        yield ajax(url2, () => {})
    }
    let it = fetch()
    let result1 = it.next()
    let result2 = it.next()
    let result3 = it.next()
    

    async函数的执行顺序

    • async中await修饰的语句会同步执行
    • async函数执行之后返回一个promise,里面执行的函数会作为回调函数在外部执行栈结束之后执行
    let c = 0
    let d = async () => {
        c = c + await 10
        console.log('函数内部1')
        await console.log('函数内部2', c)
        console.log('函数内部3')
    }
    undefined
    d()
    c++
    console.log('函数外部')
    VM2692:3 函数外部
    VM2618:4 函数内部1
    VM2618:5 函数内部2 10
    VM2618:6 函数内部3
    

    经典例题实现Promise.race和Promise.all

    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log('p1准备执行')
        resolve('p1执行了')
      }, 1000)
    })
    
    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('p2执行了')
      }, 2000)
    })
    
    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('p3执行失败了')
      }, 3000)
    })
    
    Promise.race([p1, p2, p3]).then(res => console.log(res), err => console.log(err))
    
    p1准备执行
    p1执行了
    
    Promise.all([p1, p2, p3]).then(res => console.log(res), err => console.log(err))
    
    p1准备执行
    p3执行失败了
    
    
    

    手写简版的Promise

    // <!--  简易版的promise -->
    const PENDING = "pending"
    const RESOLVE = "resolve"
    const REJECT = "reject"
    
    function MyPromise(fn) {
      const that = this
      that.status = PENDING // MyPromise 内部状态
      that.value = null // 传入 resolve 和 reject 的值
      that.resolveCallbacks = [] // 保存 then 中resolve的回调函数
      that.rejectCallbacks = [] // 保存 then 中reject的回调函数
    
      // resolve 函数 Promise内部调用 resolve 函数 例:new MyPromise((resolve,reject)=>{resolve(1)})
      function resolve(val) {
        if (that.status === PENDING) {
          that.status = RESOLVE
          that.value = val
          that.resolveCallbacks.forEach(cb => cb(that.value))
        }
      }
      // reject 函数 Promise内部调用的 reject 函数 例:new MyPromise((resolve,reject)=>{reject(1)})
      function reject(val) {
        if (that.status === PENDING) {
          that.status = REJECT
          that.value = val
          that.rejectCallbacks.forEach(cb => cb(that.value))
        }
      }
      // 调用传入 MyPromise 内的方法 例:new MyPromise((resolve,reject)=>{})   fn=(resolve,reject)=>{}
      try {
        fn(resolve, reject)
      } catch (error) {
        reject(error)
      }
    }
    // 在原型上添加then方法
    MyPromise.prototype.then = function (onFulfilled, onRejected) {
      const that = this
      // 判断传入的是否为函数
      onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
      onRejected = typeof onRejected === 'function' ? onRejected : r => {
        throw r
      }
    
      //如果 Promise 内部存在异步代码,调用then方法时,此时 promise 内部还是 PENDING 状态,
      将 then 里面的函数添加进回调数组,当异步处理完成后调用 MyPromise 内部的 resolve 或者
      reject 函数
      if (that.status === PENDING) {
        that.resolveCallbacks.push(onFulfilled)
        that.rejectCallbacks.push(onRejected)
      }
    
      // 当 Promise 内部的状态已经为 resolve,则调用 then 里面的函数并传递值
      if (that.status === RESOLVE) {
        onFulfilled(that.value)
      }
    
      // 当 Promise 内部状态为 reject,则调用then里的回调函数并传递值
      if (that.status === REJECT) {
        onRejected(that.value)
      }
    }
    
    // 自己实现的Promise
    new MyPromise((resolve, reject) => {
      setTimeout(() => {
        resolve(1)
        reject(2)
      }, 0)
    }).then(res => {
      console.log(res)
    }, err => {
      console.log(err)
    })
    // MyPromise.resolve(4).then().then(res => console.log(4)) // 透传,尚未实现
    
    

    async和await遇见EventsLoop的注意事项

    一般而言,我们可以把
    
    async function f() {
      await p
      console.log('ok')
    }
    简化理解为:
    
    function f() {
      return RESOLVE(p).then(() => {
        console.log('ok')
      })
    }
    『RESOLVE(p)』接近于『Promise.resolve(p)』,不过有微妙而重要的区别:
    p 如果本身已经是 Promise 实例,Promise.resolve 会直接返回 p 而不是产生一个新 promise。
    【个人认为 Promise.resolve 的这一『优化』行为可能是弊大于利的,不过因为已经写进标准了,
    也不太可能修改了。】老版本V8的问题是当 p 是一个已经 settled 的 promise,会进行类似的激进优化,
    导致执行时序与非 settled 的 promise 结果不同。比如把你的 async2 中加入一个 await 语句,
    老版本行为就和新版本一致了。
    

    例题:

    console.log('script start')
    
    async function async1() {
      await async2()
      console.log('async1 end')
    }
    async function async2() {
      console.log('async2 end')
    }
    async1()
    
    setTimeout(function() {
      console.log('setTimeout')
    }, 0)
    
    new Promise(resolve => {
      console.log('Promise')
      resolve()
    })
      .then(function() {
        console.log('promise1')
      })
      .then(function() {
        console.log('promise2')
      })
    
    console.log('script end')
    // script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
    
    

    解析:

    当await 后面是一个async函数并且执行之后,也就是:
    async function async1() {
      await async2()
      console.log('async1 end')
    }
    async function async2() {
      console.log('async2 end')
    }
    
    由于async2执行后返回一个promise,则相当于
    async function async1() {
       return new Promise((res, rej) => {
         console.log('async2 end')
      }).then(() => {
         console.log('async1 end')
     })
    }
    
    结果一样
    

    null和undefined有何区别

    • null表示有值,但是为空,Number(null) === 1 false
    • undefined表示没有值,还没有定义,Number(undefined) === NaN false

    如何正确判断业务中遇到的undefined值

    • 注意:避免使用==,因为
    null == undefined
    true
    0 == undefined
    false
    '' == undefined
    false
    
    • 正确判断方法
    1、使用====
      if(backgroundAudioManger === undefined) ...
    2、使用typeof
        if(typeof backgroundAdudioManager == 'undefined')
    

    手写实现call,apply,bind

    • call
    Function.prototype.myCall = function(context) {
      if ( typeof this !== 'function' ) throw new TypeError('Error')
      // 完善部分,如果传入context是个基础类型是无法绑定fn函数的,所以
      
                if (typeof context === 'object') {
                    context = context || window;
                } else {
                    context = Object.create(null)
                }
    
      context = context || window
      // 如果context中有fn则会被覆盖并清除
      // newContext.fn = this
      // 使用Symbol()独一无二数据类型避免fn冲突
      let fn = Symbol('fn')
      context[fn] = this
      let args
      let result
      if ([...arguments][1]) {
        args = [...arguments].slice(1)
        result = newContext.fn(args)
      } else {
        result = newContext.fn()
      }
      delete context[fn]
      return result
    }
    
    function fn () {
      console.log(this.a, this)
    }
    
    const obj = {
      a: 21
    }
    
    fn.myCall(obj)
    
    • apply
    Function.prototype.myApply = function(context) {
      if (typeof this !== 'function') {
        throw new TypeError('Error')
      }
      context = context || window
      context.fn = this
      let result
      // 处理参数和 call 有区别
      if (arguments[1]) {
        result = context.fn(...arguments[1])
      } else {
        result = context.fn()
      }
      delete context.fn
      return result
    }
    
    
    • bind
    Function.prototype.myBind = function (context) {
      if (typeof this !== 'function') {
        throw new TypeError('Error')
      }
      const _this = this
      const args = [...arguments].slice(1)
      // 返回一个函数
      return function F() {
        // 因为返回了一个函数,我们可以 new F(),所以需要判断
        if (this instanceof F) {
          return new _this(...args, ...arguments)
        }
        return _this.apply(context, args.concat(...arguments))
      }
    }
    

    手写实现一个new方法

    • new原理那部分,链接到原型,obj = Object.create(Con.prototype),这种方式会比较好
    function _new(fn,...arg){
        let obj = {}
        let con = [].slice.call(arguments)
        obj.__proto__ = con.prototype //链接原型
        const ret = fn.call(obj, ...arg); //改变this的指向
        return ret instanceof Object ? ret : obj;
    }
    
    

    Symbol()和Symbol.for()和Symbol.keyFor()区别

    • 参考链接:链接
    • Symbol()返回一个独一无二的标识符类型值
    • Symbol.for(),将传入值生成一个标识符类性值,并将其作为key存入到注册表中
    • Symbol.keyFor(),传入一个Symbol.for()生成的标识符类型值,返回注册表中对应该key的value值(Symbol.for(‘value’))
    • image.pngimage.png

    巧用parseFloat避免js浮点数精度转换问题

    (0.1 + 0.2).toFixed(10)
    "0.3000000000"
    parseFloat((0.1 + 0.2).toFixed(10))
    0.3
    

    更简便的手写call

    Function.prototype.myCall = function(context,...args){
      context  = context || window
      const symbol = Symbol()
      context[symbol] = this
      const result = context[symbol](...args)
      delete context[symbol]
      return result
    }
    

    [].shift.call(arguments)是什么原理呢?

    改变绑定的 this:
    
    let a = [1,2,3]
    const myshift = [].shift.bind(a)
    console.log(myshift(a))
    
    
    • Arguments是一个类数组对线,不能直接进行数组想关的操作,使用[].shifit.call(arguments)则可以转化为数组之后进行一些操作
    function fn3 (a, b) {
        console.log(arguments)
        console.log([...arguments])
        console.log([].shift.call(arguments))
    }
    
    image.pngimage.png

    <br />

    <br />如果传入的参数为函数,则[].shift.call(arguments)会弹出这个函数<br /> image.pngimage.png

    <br />

    事件代理应用

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>手写函数测试</title>
    </head>
    <body>
    <ul id="ul">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
    </ul>
    <script>
      let ul = document.querySelector('#ul')
      ul.addEventListener('click', (event) => {
        console.log(event.target);
      })
    </script>
    </body>
    </html>
    

    阻止事件冒泡

    • 操作:在子DOM节点的event执行事件上绑定stopPropagation
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>手写函数测试</title>
    </head>
    <body>
    <ul id="ul">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li id="li">4</li>
      <li>5</li>
    </ul>
    <script>
      let ul = document.querySelector('#ul')
      ul.addEventListener('click', (event) => {
        console.log(event);
      }) // 如果在后面配置项为true, 则先捕获(父节点事件)后冒泡(子节点事件)
      let li = document.querySelector('#li')
      li.addEventListener('click', (event) => {
        console.log(event, '4点击了')
        event.stopPropagation() // 阻止其余捕获
      })
    </script>
    </body>
    </html>
    

    冒泡事件和捕获事件顺序调换的配置

    • 默认false
    <div>
        外层div
        <p>内层p</p>
    <div>
    var div = document.querySelector('div');
    var p = document.querySelector('p');
    
    div.addEventListener('click',  function() {console.log("冒泡")}, false);
    div.addEventListener('click',  function() {console.log("捕获")}, true);
    
    点击div, 输出 冒泡,捕获
    点击p, 输出捕获,冒泡
    

    <br />

    浏览器同源策略要干啥的, 为什么要跨域请求?

    跨域解决方案

    • Jsonp
    • Cors白名单
    • document.domain(适用于二级域名相同--->a.test.com 和 b.test.com)
    • postMessage
    • 前端proxy正向代理
    • 后端proxy反向代理

    cookie,localStorage,sessionStorage区别

    • cookie:多为网络请求过程中服务器通过请求信息在浏览器客户端种下,可以用来保存用户登录状态,规避http请求无状态缺陷,缺陷是每一个name对应的value值大小只有4k,可存储量太小,超过会被裁剪,而且每次请求都会携带,造成额外的带宽占用
    • localStorage:除非手动清空,否则用于存在于本地存储中,可在同源标签页下共享,适用于一些长久不变的数据,比如一些电商网站的base64数据,大小约为5兆左右
    • sessionStorage:只在当前窗口下存在,同源标签下不共享,浏览器窗口关闭会被自动清空,适用于需要每次打开都重新登录的网站存储JWT的token数据。大小约为5兆左右
    • 区别:sessionStorage保存的数据用于浏览器的一次会话,当会话结束(通常是该窗口关闭),数据被清空;sessionStorage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 sessionStorage 内容便无法共享;localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。除了保存期限的长短不同,SessionStorage的属性和方法与LocalStorage完全一样。
    • 参考:https://github.com/ljianshu/Blog/issues/25

    <br />

    知道哪些新的缓存方案?

    • IndexDB:浏览器提供的用途类似localStorage的NoSQL型数据库,除非手动清除否则一直存在,类似MongoDB的异步存取,
    • 参考:阮一峰IndexDB入门
    • 参考:简书IndexDB实例

    代理的工作原理解析

    <br />

    • webpack中的proxy本地代理原理
    • 使用了express框架搭建本web项目服务的时候,引入了http-proxy-middleware的中间件,参考链接https://www.jianshu.com/p/8fd5d7347c57
    • image.pngimage.png

    node服务后台接收预检请求防止报错

    if(res.method == 'OPTIONS') {
    res.statusCode = 204
    res.setHeader('Content-Length', '0')
    res.end()
    } else {
    next()
    }
    
    

    浏览器缓存机制(后端向)

    [图片上传失败...(image-f02655-1611839965980)]<br />

    浏览器渲染原理

    参考地址:https://github.com/ljianshu/Blog/issues/51

    • 接收信息解析代码过程:

    [图片上传失败...(image-f04011-1611839965980)]

    1. 接收HTML文件,构建DOM树
    2. 接收CSS文件,构建CSSOM树
    3. 接收jS文件(加载js脚本),等到Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。

    <br />

    • 注意:在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
    • 浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM

    <br />

    性能优化建议:

    • 还有js中操作dom元素的样式时,如果修改多条style,不如把多条style写成一个class,对元素进行添加class的操作。如果是分步添加元素样式,可以将元素赋值给一个变量,避免多次获取一个元素,因为获取元素也会造成回流。
      1. 不要一条一条的修改 DOM 样式,尽量提前设置好 class,后续增加 class,进行批量修改。
      1. 使用 documentFragment 对象在内存里操作 DOM
      1. 不一定说非要用visibility: hidden替换display:none,完全可以把修改频繁的元素先 display: none,修改完之后显示。

    RAF讨论

    • 屏幕每16.6ms才刷新一次,比如这一帧有个人站着,下一帧他应该要迈开左腿,如果都已经下一帧了,浏览器还没渲染出迈开腿的画面,这个人还站着,看着就卡了。然后event loop和渲染这边,我的理解是,在短短16.6ms内,要保证一个tick能执行完并将这个tick里对dom的操作渲染出来(也因此同个tick里的多个dom操作会被合并),这样下一帧才能有迈开腿的画面出现在屏幕上。如果当前的tick要花好久才能执行完,就说40ms吧,那至少两帧都只有人站着的画面,第三帧还剩短短10ms用来重新渲染,如果能渲染出来还好,下一帧就能看,否则下一帧还看不到变化。
    • 所以,假设一个tick里js先往某个<ul>里添加了几个<li>子节点,接着还要做一件非常耗时的事情,两件事合在一起要好久才能完成,不如把两件事拆开,放到两个requestAnimationFrame的回调里执行,这样就能先把对dom的修改重新渲染处理,下一帧至少能看到变化了。

    <br />

    前端安全

    参考:XSS攻击和CSRF攻击

    XSS:https://juejin.im/post/5bad9140e51d450e935c6d64<br />CSRF:https://juejin.im/post/5bc009996fb9a05d0a055192#heading-32<br />

    了解webpack原理,手写小型webpack

    流程浅析:参考

    1. createAsset:读取入口文件,通过babylon的parse方法将文件内容字符串转化为AST语法树(包含id,filename,dependencies,code信息)的一种数据结构
    2. createGraph:遍历入门文件AST语法树上的dependencies数组,生成一个新的数组,数组每一项和createAsset方法创建出来的AST语法树非常相像,并且多了一个mapping属性指定文件依赖,数组中将依赖文件根据依赖顺序id递增排列。
    3. bundle:接收graph数组,生成bundle结构的自执行函数
    4. 代码地址:https://github.com/dykily/simple_webpack/blob/327618d4e2/bundler.js

    相关文章

      网友评论

          本文标题:常见面试题总结(一)

          本文链接:https://www.haomeiwen.com/subject/qqjslktx.html