常见面试题总结(三)

1,155 阅读18分钟

记录常见面试题,加入个人理解,不保证准确性,欢迎交流指正嗷~


new实现原理及手写一个new方法

new操作符原理

1、创建一个空对象


2、让空对象的__proto__(IE没有该属性)成员指向了构造函数的prototype成员对象


3、使用apply调用构造器函数,属性和方法被添加到 this 引用的对象中


4、如果构造函数中没有返回其它对象,那么返回 this,即创建的这个的新对象,否则,返回构造函数中返回的对象

手写实现一个new方法

1、

function _new(func) {
    // 第一步 创建新对象
    let obj= {}; 
    // 第二步 空对象的_proto_指向了构造函数的prototype成员对象
    obj.__proto__ = func.prototype;//
    // 一二步合并就相当于 let obj=Object.create(func.prototype)

    // 第三步 使用apply调用构造器函数,属性和方法被添加到 this 引用的对象中
    let result = func.apply(obj);
    if (result && (typeof (result) == "object" || typeof (result) == "function")) {
    // 如果构造函数执行的结果返回的是一个对象,那么返回这个对象
        return result;
    }
    // 如果构造函数返回的不是一个对象,返回创建的新对象
    return obj;
}

// 测试
function PP(name) {
	this.name = name
}

PP.prototype.sayName = function() { console.log(this.name) }

const p1 = _new(Person)
console.log(p1 instanceof Person) // true
console.log(p1.__proto__ === Person.prototype) // true

2、

function Person(name) {
    this.name = name;
}
Person.prototype.sayName = function () {
    console.log(this.name);
}

function createPerson() {
    // 1 创建一个新的对象
    var o = {};
    // 2 获取构造函数,以便实现作用域的绑定
    console.log(arguments)
    var _constructor = [].shift.call(arguments);
    console.log(_constructor)
    // 3 由于通过new操作创建的对象实例内部的不可访问的属性[[Prototype]](有些浏览器里面为__proto__ "[Prototype]")
    //指向的是构造函数的原型对象的,所以这里实现手动绑定。
    o.__proto__ = _constructor.prototype;
    // 4.作用域的绑定使用apply改变this的指向
    _constructor.apply(o, arguments);
    return o;
}
var person1 = createPerson(Person, 'ydb');
person1.sayName();

VM393:12 Arguments(2) [ƒ, "ydb", callee: ƒ, Symbol(Symbol.iterator): ƒ]0: "ydb"callee: ƒ createPerson()length: 1Symbol(Symbol.iterator): ƒ values()__proto__: Object
VM393:14 ƒ Person(name) {
    this.name = name;
}
VM393:5 ydb
function _new(Func, ...args) {
	const obj = Object.create(Func.prototype)
  let result = Func.call(obj, ...args)
  if(result != null && /^object|function/.test(typeof result)) {
  	return result
  }
  return obj
}

实现继承的几种方式


😊总结:寄生组合继承和ES6 extends为最佳继承方式

有缺陷,不推荐:

1.原型链继承:将父类的实例作为子类的原型

    function Son(name) {
      this.name = name || "son";
    }

    Son.prototype = new Father();
优点:
1. 简单,易于实现
2. 父类新增原型方法、原型属性,子类都能访问到
缺点:
1. 无法实现多继承,因为原型一次只能被一个实例更改
2. 来自原型对象的所有属性被所有实例共享(上诉例子中的color属性)
3. 创建子类实例时,无法向父构造函数传参
2.构造继承继承:复制父类的实例属性给子类

    function Son(name) {
      Father.call(this, "我是传给父类的参数");
      this.name = name || "son";
    }
优点:
1. 解决了原型链继承中子类实例共享父类引用属性的问题
2. 创建子类实例时,可以向父类传递参数
3. 可以实现多继承(call多个父类对象)
缺点:
1. 实例并不是父类的实例,只是子类的实例
2. 只能继承父类实例的属性和方法,不能继承其原型上的属性和方法
3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

推荐使用的方法

3.组合继承:将原型链和借用构造函数的技术组合到一块。
使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承

 function Son(name) {
     // 第一次调用父类构造器 子类实例增加父类实例
      Father.call(this, "我是传给父类的参数");
      this.name = name || "son";
    }
    // 经过new运算符 第二次调用父类构造器 子类原型也增加了父类实例
    Son.prototype = new Father();

优点:
1. 弥补了构造继承的缺点,现在既可以继承实例的属性和方法,也可以继承原型的属性和方法
2. 既是子类的实例,也是父类的实例
3. 可以向父类传递参数
4. 函数可以复用
缺点:
1. 调用了两次父类构造函数,生成了两份实例
2. constructor指向问题

4.实例继承:为父类实例添加新特征,作为子类实例返回

     function Son(name) {
      let f=new Father('传给父类的参数')
      f.name=name||'son'
      return f
    }
优点:
1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
缺点:
1. 实例是父类的实例,不是子类的实例
2. 不支持多继承

5.寄生组合继承:通过寄生方式,砍掉父类的实例属性,避免了组合继承生成两份实例的缺点

    function Son(name) {
      // 1、在子构造函数中执行父类构造函数传入子构造函数this
      // 实现实例属性继承
      Father.call(this);
      // 2、如果子类还有实例属性则继续绑定
      this.name = name || "son";
    }
    // 方法二  直接借用Object.create()方法
		// 3、使用Object.create方法,传入父prototype,生成一个具有原型链的对象给子prototype
		// 相当于:Son.prototype = new Father()
    Son.prototype = Object.create(Father.prototype);
    // 修复构造函数指向
    Son.prototype.constructor = Son;
优点:
1. 比较完美(js实现继承首选方式)
缺点:
1.实现起来较为复杂(可通过Object.create简化)

7.es6--Class继承:使用extends表明继承自哪个父类,并且在子类构造函数中必须调用super

    class Son extends Father {
      constructor(name) {
        super(name);
        this.name = name || "son";
      }
    }

拓展知识:

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 参数:proto新创建对象的原型对象。 返回值:一个新对象,带着指定的原型对象和属性。 多用于实现类继承

Object.create实现类继承经典案例

// Shape - 父类(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父类的方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
  Shape.call(this); // call super constructor.
}

// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?',
  rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?',
  rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'

Object.create+Object.assign实现多继承

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do a thing
};

Object.assing(a, b)有返回值的同时,也已经将a改变引用了 image.png 而b的引用不变 image.png 如果不想改变目标对象值可以用空对象中继:

const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };
const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }

防抖和节流简单实现

防抖

  • 动作停止后的时间超过设定的时间时执行一次函数。注意:这里的动作停止表示你停止了触发这个函数,从这个时间点开始计算,当间隔时间等于你设定时间,才会执行里面的回调函数。如果你一直在触发这个函数并且两次触发间隔小于设定时间,则函数一直不会执行。
防抖:动作停止后的时间超过设定的时间时执行一次函数。注意:这里的动作停止表示你停止了触发这个函数,从这个时间点开始计算,当间隔时间等于你设定时间,才会执行里面的回调函数。如果你一直在触发这个函数并且两次触发间隔小于设定时间,则函数一直不会执行。

function debance(fn, delay) {
  let timer = null;
  return () => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn();
    }, delay);
  };
}

window.addEventListener(
  "scroll",
  debance(() => {
    console.log(111);
  }, 1000)
);


防抖应用场景:
1. search搜索联想,用户在不断输入值时,用防抖来节约请求资源
2. window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

节流

  • 一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。

标识位实现:

// 方法1:使用标志位 
function throttle(fn, delay) {
      let flag = true;
      return () => {
        // 第二次由于为假则不执行直接返回,假如第四次时间到了则执行fn,同时设置为真
        // 第五次进入设置flag为假,并开启定时器,第六次return,第七次return
        // 第八次执行并设置为真,继续执行
        if (!flag) return; 
        flag = false; // 第一次进入执行flag为假
        timer = setTimeout(() => {
          fn();
          flag = true; // 到时间后执行函数并将flag设置为真
        }, delay);
      };
    }

测试:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>测试节流函数</title>
</head>
<body>
<button id="button">点击</button>
<script>
  function throttle (func, delay) {
    let flag = true;
    return function () {
      if(!flag) return
      flag = false
      setTimeout(() => {
        func()
        flag = true
      }, delay)
    }
  }
  button.onclick = throttle(() => console.log('试试'), 1000)
</script>
</body>
</html>

防抖应用场景:
1. search搜索联想,用户在不断输入值时,用防抖来节约请求资源
2. window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

时间戳实现:

  function throttle(fn, delay) {
    let startTime=new Date() // 定义startTime为当前时间
    return () => {
      let endTime=new Date() // 定义结束时间为一段时间后的当前时间
      if (endTime-startTime>=delay){ // 如果现在超过或者到了延迟时间
        fn() // 就执行函数
        // startTime=endTime // 让当前时间等于现在时间或者刚才的结束时间都行
        startTime=new Date() // 精确来讲需要等于过去时间
      }else{
        return
      }
    };
  }


    window.addEventListener(
      "scroll",
      throttle(() => {
        console.log(111);
      }, 1000)
    );

测试:

  function throttle (func, delay) {
    let startTime = new Date()
    return () => {
      let endTime = new Date()
      if(endTime - startTime >= delay) {
        func()
        startTime = endTime()
      }
    }
  }

	button.onclick = throttle(() => console.log('滚动了。。'), 1000)

节流应用场景:
1. 鼠标不断点击触发,mousedown(单位时间内只触发一次)
2. 监听滚动事件,比如是否滑到底部自动加载更多(懒加载),用throttle来判断

异步任务是都会被立即塞进任务队列吗?

  • 不,异步任务会先到Event Table(事件列表)中注册函数,如果该事件触发了,则会被移到任务队列中(DOM操作对应DOM事件,资源加载操作对应加载事件,定时器操作可以看做对应一个“时间到了”的事件)

拓展:

1、js为什么是单线程,标准解答

  • js作为浏览器脚本语言,其主要用途是与用户互动,以及操作DOM。这就决定了它只能是单线程,否则会带来很复杂的同步问题。(假设JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?)

2、宏任务微任务区别

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval, setImmediate, I/O, UI rendering
  • micro-task(微任务):Promise,process.nextTick,MutationObserver

3、微任务为什么要先于宏任务执行?

减少更新时的渲染次数 因为根据HTML标准,会在宏任务执行结束之后,在下一个宏任务开始执行之前,UI都会重新渲染。如果在microtask中就完成数据更新,当 macro-task结束就可以得到最新的UI了。如果新建一个 macro-task来做数据更新的话,那么渲染会执行两次

4、事件循环是什么?标准解答

  1. 整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;
  2. 同步任务会直接进入主线程依次执行;
  3. 异步任务会再分为宏任务和微任务;
  4. 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
  5. 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
  6. 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
  7. 上述过程会不断重复,这就是Event Loop事件循环;

image.png

如何取消一个promise

      // 方法一 取消promise方法   promise.race方法
      function wrap(p) {
        let obj = {};
        let p1 = new Promise((resolve, reject) => {
          obj.resolve = resolve;
          obj.reject = reject;
        });
        obj.promise = Promise.race([p1, p]);
        return obj;
      }

      let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(123);
        }, 1000);
      });
      let obj = wrap(promise);
      obj.promise.then(res => {
        console.log(res);
      });
      obj.resolve("请求被拦截了");

      obj.reject("请求被拒绝了");


      //方法二 取消promise方法   新包装一个可操控的promise

      function wrap(p) {
        let res = null;
        let abort = null;

        let p1 = new Promise((resolve, reject) => {
          res = resolve;
          abort = reject;
        });

        p1.abort = abort;
        p.then(res, abort);

        return p1;
      }

      let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(123);
        }, 1000);
      });
      let obj = wrap(promise);
      obj.then(res => {
        console.log(res);
      });
      obj.abort("请求被拦截");

get和post的区别

GET请求会被浏览器主动cache,而POST不会,除非手动设置

get把请求的参数放在url上,即HTTP协议头上 post把参数放在HTTP的包体内

Get 方式传输的数据量非常小,一般限制在 2 KB 左右,但是执行效率却比 >Post 方法好;而 Post
方式传递的数据量相对较大,它是等待服务器来读取数>据,不过也有字节限制(实际上IIS4中最大量
为80KB,IIS5中为100KB),这是为>了避免对服务器用大量数据进行恶意攻击

GET请求只能进行url编码,而POST支持多种编码方式

GET产生的URL地址可以加入书签,而POST不可以

GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留

GETPOST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息

http持久化连接和管道化

在HTTP1.0中,默认的是短连接,没有正式规定 Connection:Keep-alive 操作;
HTTP/1.1所有连接都是Keep-alive的,也就是默认都是持续连接的。在事务处理结束之后仍然保持在打开状态的TCP连接称之为持久连接
持久连接详解:

持久连接会在不同事务之间保持打开状态,直到客户端或服务器决定其关闭为止。重用已对目标服务器打开的空闲持久连接,就可以避开缓慢的连接建立阶段。而且,已经打开的连接还可以避免慢启动的拥塞适应阶段,以便更快速地进行数据传输。所以,持久连接降低了时延和连接建立的开销,将连接保持在已调谐状态,而且减少了打开连接的潜在数量

HTTP/1.1允许在持久连接上可选的使用请求管道,是相对于keep-alive连接的又一性能优化。在响应到达之前,可以将多条请求放入队列,当第一条请求通过网络流向服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络的环回时间,提高性能

浏览器同源策略是什么?

  • 同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键安全机制。

  • 同源策略限制:Cookie、LocalStorage 和 IndexDB无法读取。 Dom无法获得 Ajax请求不能发送

CSRF防御

  1. 增加token 在请求中放入攻击者所不能伪造的信息,并且该信总不存在于cookie之中。鉴于此,系统开发人员可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务端进行token校验,如果请求中没有token或者token内容不正确,则认为是CSRF攻击而拒绝该请求
  2. 通过Referer识别 根据HTTP协议,在HTTP头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。在通常情况下,访问一个安全受限的页面的请求都来自于同一个网站
  3. 网站重要操作增加验证码 CSRF攻击过程中,用户在不知情的情况下构造了网络请求,添加验证码后,强制用户必须与应用进行交互

浏览器渲染过程标准答案

  • 过程:

    1. 解析HTML,生成DOM树(DOM)
    2. 解析CSS,生成CSSOM树(CSSOM)
    3. 将DOM和CSSOM合并,生成渲染树(Render-Tree)
    4. 计算渲染树的布局(Layout)
    5. 将布局渲染到屏幕上(Paint)

关键概念

CSS阻塞渲染:由于CSSOM负责存储渲染信息,浏览器就必须保证在合成渲染树之前,CSSOM是完备的,这种完备是指所有的CSS(内联、内部和外部)都已经下载完,并解析完,只有CSSOM和DOM的解析完全结束,浏览器才会进入下一步的渲染。CSS阻塞渲染意味着,在CSSOM完备前,页面将一直处理白屏状态,这就是为什么样式放在head中,仅仅是为了更快的解析CSS,保证更快的首次渲染。
JS阻塞页面:JS可以操作DOM来修改DOM结构,可以操作CSSOM来修改节点样式,这就导致了浏览器在解析HTML时,一旦碰到script,就会立即停止HTML的解析,也阻塞了其后的CSS解析,整个解析进程必须等待JS的执行完成才能够继续。从性能角度上讲,将script放在页面底部,也就合情合理了
重排(Reflow):DOM结构中的各个元素都有自己的盒子(模型),这些都需要浏览器根据各种样式来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow
触发重排(reflow):

  • 1、当增加、删除、修改DOM节点时,会导致reflow或repaint
  • 2、当移动DOM的位置,或是插入动画的时候
  • 3、当修改CSS样式的时候
  • 4、当Resize窗口的时候,或是滚动的时候
  • 5、当修改网页的默认字体时

重绘(Repaint):当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称之为repaint。
触发重绘(Repaint):

  • 1、DOM改动
  • 2、CSS改动

_最小化重绘和重排_1.一次性修改样式:减少内联样式使用 样式合并写法 2.批量修改DOM:使用文档片段创建一个子树,然后再拷贝到文档中(document.fragment) 3.缓存布局信息

//每次需要查询div.offsetLeft 浪费性能
div.style.left = 1 + div.offsetLeft + 'px';
div.style.top = 1 + div.offsetTop + 'px';

//将这个值保存下来,避免重复取值 性能优化
current = div.offsetLeft;
div.style.left = 1 + ++current + 'px';
div.style.top = 1 + ++current + 'px';

前端性能优化建议

前端常用方法:

  1. 资源合并压缩 减少HTTP请求
    1.1 尽量合并和压缩html css和js文件 借助前端工具 例如 webpack gulp grunt... 1.2 开启gzip压缩
  2. 图片优化
    2.1 雪碧图 图片压缩 svg base64
  3. 懒加载 / 预加载
    3.1 懒加载:图片进入可视区域之后请求图片资源 对于电商等图片很多,页面很长的业务场景适用 并发加载的资源过多会阻塞 js 的加载,影响网站的正常使用
    3.2 预加载:图片等静态资源在使用之前的提前请求 资源使用到时能从缓存中加载,提升用户体验
  4. 浏览器存储 localStorage:大小为 5M 左右仅在客户端使用,不和服务端进行通信 浏览器本地缓存方案 indexedDB:用于客户端存储大量结构化数据 为应用创建离线版本
  5. 浏览器缓存5.1强缓存:
    • expires: 缓存过期时间,用来指定资源到期的时间,是服务器端的绝对时间 告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求
    • cache-control:max-age = xxx 声明该资源在加载后的xxx秒内都直接使用缓存 使用的是相对时间 即加载文件本机的时间
      如果在Cache-Control响应头设置了 "max-age" 或者 "s-max-age" 指令,那么 Expires 头会被忽略。
  6. 5.2协商缓存:

触发条件

  1. Cache-Control 的值为 no-cache (不强缓存)
  2. max-age 过期了 (强缓存,但总有过期的时候)
  • Last-Modified / If-Modified-Since
  1. Last-Modified ------- response header
    If-Modified-Since ------- request header
    缺点:某些服务端不能获取精确的修改时间;文件修改时间改了,但文件内容却没有变
    • Etag / If-None-Match
  2. 文件内容的 hash 值
    etag ------- response header
    if-none-match ------- request header
  3. CDN 内容分发网络

Proxy 相比于 defineProperty 的优势

  • 数组变化也能监听到
  • 不需要深度遍历监听
let data = { a: 1 }
let reactiveData = new Proxy(data, {
	get: function(target, name){
		// ...
	},
	// ...
})