关于前端:美团前端一面高频面试题

7次阅读

共计 15166 个字符,预计需要花费 38 分钟才能阅读完成。

CSS 优化和进步性能的办法有哪些?

加载性能:

(1)css 压缩:将写好的 css 进行打包压缩,能够减小文件体积。

(2)css 繁多款式:当须要下边距和右边距的时候,很多时候会抉择应用 margin:top 0 bottom 0;但 margin-bottom:bottom;margin-left:left; 执行效率会更高。

(3)缩小应用 @import,倡议应用 link,因为后者在页面加载时一起加载,前者是期待页面加载实现之后再进行加载。

选择器性能:

(1)要害选择器(key selector)。选择器的最初面的局部为要害选择器(即用来匹配指标元素的局部)。CSS 选择符是从右到左进行匹配的。当应用后辈选择器的时候,浏览器会遍历所有子元素来确定是否是指定的元素等等;

(2)如果规定领有 ID 选择器作为其要害选择器,则不要为规定减少标签。过滤掉无关的规定(这样款式零碎就不会浪费时间去匹配它们了)。

(3)防止应用通配规定,如 *{}计算次数惊人,只对须要用到的元素进行抉择。

(4)尽量少的去对标签进行抉择,而是用 class。

(5)尽量少的去应用后辈选择器,升高选择器的权重值。后辈选择器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三层,更多的应用类来关联每一个标签元素。

(6)理解哪些属性是能够通过继承而来的,而后防止对这些属性反复指定规定。

渲染性能:

(1)谨慎应用高性能属性:浮动、定位。

(2)尽量减少页面重排、重绘。

(3)去除空规定:{}。空规定的产生起因一般来说是为了预留款式。去除这些空规定无疑能缩小 css 文档体积。

(4)属性值为 0 时,不加单位。

(5)属性值为浮动小数 0.**,能够省略小数点之前的 0。

(6)标准化各种浏览器前缀:带浏览器前缀的在前。规范属性在后。

(7)不应用 @import 前缀,它会影响 css 的加载速度。

(8)选择器优化嵌套,尽量避免层级过深。

(9)css 雪碧图,同一页面相近局部的小图标,方便使用,缩小页面的申请次数,然而同时图片自身会变大,应用时,优劣思考分明,再应用。

(10)正确应用 display 的属性,因为 display 的作用,某些款式组合会有效,徒增款式体积的同时也影响解析性能。

(11)不滥用 web 字体。对于中文网站来说 WebFonts 可能很生疏,国外却很风行。web fonts 通常体积宏大,而且一些浏览器在下载 web fonts 时会阻塞页面渲染伤害性能。

可维护性、健壮性:

(1)将具备雷同属性的款式抽离进去,整合并通过 class 在页面中进行应用,进步 css 的可维护性。

(2)款式与内容拆散:将 css 代码定义到内部 css 中。

说一下怎么取出数组最多的一项?

// 我这里只是一个示例
const d = {};
let ary = ['赵', '钱', '孙', '孙', '李', '周', '李', '周', '周', '李'];
ary.forEach(k => !d[k] ? d[k] = 1 : d[k]++);
const result = Object.keys(d).sort((a, b) => d[b] - d[a]).filter((k, i, l) => d[k] === d[l[0]]);
console.log(result)

二分查找 – 工夫复杂度 log2(n)

题目形容: 如何确定一个数在一个有序数组中的地位

实现代码如下:

function search(arr, target, start, end) {
  let targetIndex = -1;

  let mid = Math.floor((start + end) / 2);

  if (arr[mid] === target) {
    targetIndex = mid;
    return targetIndex;
  }

  if (start >= end) {return targetIndex;}

  if (arr[mid] < target) {return search(arr, target, mid + 1, end);
  } else {return search(arr, target, start, mid - 1);
  }
}
// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// const position = search(dataArr, 6, 0, dataArr.length - 1);
// if (position !== -1) {//   console.log(` 指标元素在数组中的地位:${position}`);
// } else {//   console.log("指标元素不在数组中");
// }

Set 和 Map 有什么区别?

1、Map 是键值对,Set 是值得汇合,当然键和值能够是任何得值
2、Map 能够通过 get 办法获取值,而 set 不能因为它只有值
3、都能通过迭代器进行 for...of 遍历
4、Set 的值是惟一的能够做数组去重,而 Map 因为没有格局限度,能够做数据存储

说说浏览器缓存

缓存能够缩小网络 IO 耗费,进步访问速度。浏览器缓存是一种操作简略、效果显著的前端性能优化伎俩
很多时候,大家偏向于将浏览器缓存简略地了解为“HTTP 缓存”。但事实上,浏览器缓存机制有四个方面,它们依照获取资源时申请的优先级顺次排列如下:Memory Cache
Service Worker Cache
HTTP Cache
Push Cache

缓存它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的状况下,才会走协商缓存
    实现强缓存,过来咱们始终用 expires。当服务器返回响应时,在 Response Headers 中将过期工夫写入 expires 字段,当初个别应用 Cache-Control 两者同时呈现应用 Cache-Control         协商缓存,Last-Modified 是一个工夫戳,如果咱们启用了协商缓存,它会在首次申请时随着 Response Headers 返回:每次申请去判断这个工夫戳是否发生变化。从而去决定你是 304 读取缓存还是给你返回最新的数据

PWA 应用过吗?serviceWorker 的应用原理是啥?

渐进式网络应用(PWA)是谷歌在 2015 年底提出的概念。基本上算是 web 应用程序,但在外观和感觉上与 原生 app相似。反对 PWA 的网站能够提供脱机工作、推送告诉和设施硬件拜访等性能。

Service Worker是浏览器在后盾独立于网页运行的脚本,它关上了通向不须要网页或用户交互的性能的大门。当初,它们已包含如推送告诉和后盾同步等性能。未来,Service Worker将会反对如定期同步或天文围栏等其余性能。本教程探讨的外围性能是拦挡和解决网络申请,包含通过程序来治理缓存中的响应。

GET 办法 URL 长度限度的起因

实际上 HTTP 协定标准并没有对 get 办法申请的 url 长度进行限度,这个限度是特定的浏览器及服务器对它的限度。
IE 对 URL 长度的限度是 2083 字节(2K+35)。因为 IE 浏览器对 URL 长度的允许值是最小的,所以开发过程中,只有 URL 不超过 2083 字节,那么在所有浏览器中工作都不会有问题。

GET 的长度值 = URL(2083)-(你的 Domain+Path)-2(2 是 get 申请中?= 两个字符的长度)

上面看一下支流浏览器对 get 办法中 url 的长度限度范畴:

  • Microsoft Internet Explorer (Browser):IE 浏览器对 URL 的最大限度为 2083 个字符,如果超过这个数字,提交按钮没有任何反馈。
  • Firefox (Browser):对于 Firefox 浏览器 URL 的长度限度为 65,536 个字符。
  • Safari (Browser):URL 最大长度限度为 80,000 个字符。
  • Opera (Browser):URL 最大长度限度为 190,000 个字符。
  • Google (chrome):URL 最大长度限度为 8182 个字符。

支流的服务器对 get 办法中 url 的长度限度范畴:

  • Apache (Server):能承受最大 url 长度为 8192 个字符。
  • Microsoft Internet Information Server(IIS):能承受最大 url 的长度为 16384 个字符。

依据下面的数据,能够晓得,get 办法中的 URL 长度最长不超过 2083 个字符,这样所有的浏览器和服务器都可能失常工作。

手写公布订阅

class EventListener {listeners = {};
    on(name, fn) {(this.listeners[name] || (this.listeners[name] = [])).push(fn)
    }
    once(name, fn) {let tem = (...args) => {this.removeListener(name, fn)
            fn(...args)
        }
        fn.fn = tem
        this.on(name, tem)
    }
    removeListener(name, fn) {if (this.listeners[name]) {this.listeners[name] = this.listeners[name].filter(listener => (listener != fn && listener != fn.fn))
        }
    }
    removeAllListeners(name) {if (name && this.listeners[name]) delete this.listeners[name]
        this.listeners = {}}
    emit(name, ...args) {if (this.listeners[name]) {this.listeners[name].forEach(fn => fn.call(this, ...args))
        }
    }
}

其余值到数字值的转换规则?

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • Boolean 类型的值,true 转换为 1,false 转换为 0。
  • String 类型的值转换如同应用 Number() 函数进行转换,如果蕴含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。
  • 对象(包含数组)会首先被转换为相应的根本类型值,如果返回的是非数字的根本类型值,则再遵循以上规定将其强制转换为数字。

为了将值转换为相应的根本类型值,形象操作 ToPrimitive 会首先(通过外部操作 DefaultValue)查看该值是否有 valueOf()办法。如果有并且返回根本类型值,就应用该值进行强制类型转换。如果没有就应用 toString() 的返回值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回根本类型值,会产生 TypeError 谬误。

浏览器的渲染过程

浏览器渲染次要有以下步骤:

  • 首先解析收到的文档,依据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
  • 而后对 CSS 进行解析,生成 CSSOM 规定树。
  • 依据 DOM 树和 CSSOM 规定树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个蕴含有色彩和大小等属性的矩形,渲染对象和 DOM 元素绝对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM 元素对应几个可见对象,它们个别是一些具备简单构造的元素,无奈用一个矩形来形容。
  • 当渲染对象被创立并增加到树中,它们并没有地位和大小,所以当浏览器生成渲染树当前,就会依据渲染树来进行布局(也能够叫做回流)。这一阶段浏览器要做的事件是要弄清楚各个节点在页面中的确切地位和大小。通常这一行为也被称为“主动重排”。
  • 布局阶段完结后是绘制阶段,遍历渲染树并调用渲染对象的 paint 办法将它们的内容显示在屏幕上,绘制应用 UI 根底组件。

大抵过程如图所示:

留神: 这个过程是逐渐实现的,为了更好的用户体验,渲染引擎将会尽可能早的将内容出现到屏幕上,并不会等到所有的 html 都解析实现之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

Promise 以及相干办法的实现

题目形容: 手写 Promise 以及 Promise.all Promise.race 的实现

实现代码如下:

class Mypromise {constructor(fn) {
    // 示意状态
    this.state = "pending";
    // 示意 then 注册的胜利函数
    this.successFun = [];
    // 示意 then 注册的失败函数
    this.failFun = [];

    let resolve = (val) => {
      // 放弃状态扭转不可变(resolve 和 reject 只准触发一种)if (this.state !== "pending") return;

      // 胜利触发机会  扭转状态 同时执行在 then 注册的回调事件
      this.state = "success";
      // 为了保障 then 事件先注册(次要是思考在 promise 外面写同步代码)promise 标准 这里为模仿异步
      setTimeout(() => {
        // 执行以后事件外面所有的注册函数
        this.successFun.forEach((item) => item.call(this, val));
      });
    };

    let reject = (err) => {if (this.state !== "pending") return;
      // 失败触发机会  扭转状态 同时执行在 then 注册的回调事件
      this.state = "fail";
      // 为了保障 then 事件先注册(次要是思考在 promise 外面写同步代码)promise 标准 这里模仿异步
      setTimeout(() => {this.failFun.forEach((item) => item.call(this, err));
      });
    };
    // 调用函数
    try {fn(resolve, reject);
    } catch (error) {reject(error);
    }
  }

  // 实例办法 then

  then(resolveCallback, rejectCallback) {
    // 判断回调是否是函数
    resolveCallback =
      typeof resolveCallback !== "function" ? (v) => v : resolveCallback;
    rejectCallback =
      typeof rejectCallback !== "function"
        ? (err) => {throw err;}
        : rejectCallback;
    // 为了放弃链式调用  持续返回 promise
    return new Mypromise((resolve, reject) => {
      // 将回调注册到 successFun 事件汇合外面去
      this.successFun.push((val) => {
        try {
          //    执行回调函数
          let x = resolveCallback(val);
          //(最难的一点)// 如果回调函数后果是一般值 那么就 resolve 进来给下一个 then 链式调用  如果是一个 promise 对象(代表又是一个异步)那么调用 x 的 then 办法 将 resolve 和 reject 传进去 等到 x 外部的异步 执行结束的时候(状态实现)就会主动执行传入的 resolve 这样就管制了链式调用的程序
          x instanceof Mypromise ? x.then(resolve, reject) : resolve(x);
        } catch (error) {reject(error);
        }
      });

      this.failFun.push((val) => {
        try {
          //    执行回调函数
          let x = rejectCallback(val);
          x instanceof Mypromise ? x.then(resolve, reject) : reject(x);
        } catch (error) {reject(error);
        }
      });
    });
  }
  // 静态方法
  static all(promiseArr) {let result = [];
    // 申明一个计数器 每一个 promise 返回就加一
    let count = 0;
    return new Mypromise((resolve, reject) => {for (let i = 0; i < promiseArr.length; i++) {
      // 这里用 Promise.resolve 包装一下 避免不是 Promise 类型传进来
        Promise.resolve(promiseArr[i]).then((res) => {// 这里不能间接 push 数组  因为要管制程序一一对应(感激评论区斧正)
            result[i] = res;
            count++;
            // 只有全副的 promise 执行胜利之后才 resolve 进来
            if (count === promiseArr.length) {resolve(result);
            }
          },
          (err) => {reject(err);
          }
        );
      }
    });
  }
  // 静态方法
  static race(promiseArr) {return new Mypromise((resolve, reject) => {for (let i = 0; i < promiseArr.length; i++) {Promise.resolve(promiseArr[i]).then((res) => {
            //promise 数组只有有任何一个 promise 状态变更  就能够返回
            resolve(res);
          },
          (err) => {reject(err);
          }
        );
      }
    });
  }
}

// 应用
// let promise1 = new Mypromise((resolve, reject) => {//   setTimeout(() => {//     resolve(123);
//   }, 2000);
// });
// let promise2 = new Mypromise((resolve, reject) => {//   setTimeout(() => {//     resolve(1234);
//   }, 1000);
// });

// Mypromise.all([promise1,promise2]).then(res=>{//   console.log(res);
// })

// Mypromise.race([promise1, promise2]).then(res => {//   console.log(res);
// });

// promise1
//   .then(
//     res => {//       console.log(res); // 过两秒输入 123
//       return new Mypromise((resolve, reject) => {//         setTimeout(() => {//           resolve("success");
//         }, 1000);
//       });
//     },
//     err => {//       console.log(err);
//     }
//   )
//   .then(
//     res => {//       console.log(res); // 再过一秒输入 success
//     },
//     err => {//       console.log(err);
//     }
//   );

扩大思考: 如何勾销 promise

Promise.race()办法能够用来竞争 Promise
能够借助这个个性 本人包装一个 空的 Promise 与要发动的 Promise 来实现

function wrap(pro) {let obj = {};
  // 结构一个新的 promise 用来竞争
  let p1 = new Promise((resolve, reject) => {
    obj.resolve = resolve;
    obj.reject = reject;
  });

  obj.promise = Promise.race([p1, pro]);
  return obj;
}

let testPro = new Promise((resolve, reject) => {setTimeout(() => {resolve(123);
  }, 1000);
});

let wrapPro = wrap(testPro);
wrapPro.promise.then((res) => {console.log(res);
});
wrapPro.resolve("被拦挡了");

对 rest 参数的了解

扩大运算符被用在函数形参上时,它还能够把一个拆散的参数序列整合成一个数组

function mutiple(...args) {
  let result = 1;
  for (var val of args) {result *= val;}
  return result;
}
mutiple(1, 2, 3, 4) // 24

这里,传入 mutiple 的是四个拆散的参数,然而如果在 mutiple 函数里尝试输入 args 的值,会发现它是一个数组:

function mutiple(...args) {console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

这就是 … rest 运算符的又一层威力了,它能够把函数的多个入参收敛进一个数组里。这一点 常常用于获取函数的多余参数,或者像下面这样解决函数参数个数不确定的状况。

寄生组合继承

题目形容: 实现一个你认为不错的 js 继承形式

实现代码如下:

function Parent(name) {
  this.name = name;
  this.say = () => {console.log(111);
  };
}
Parent.prototype.play = () => {console.log(222);
};
function Children(name) {Parent.call(this);
  this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
// let child = new Children("111");
// // console.log(child.name);
// // child.say();
// // child.play();

代码输入后果

function foo() {console.log( this.a);
}

function doFoo() {foo();
}

var obj = {
  a: 1,
  doFoo: doFoo
};

var a = 2; 
obj.doFoo()

输入后果:2

在 Javascript 中,this 指向函数执行时的以后对象。在执行 foo 的时候,执行环境就是 doFoo 函数,执行环境为全局。所以,foo 中的 this 是指向 window 的,所以会打印出 2。

什么是物理像素,逻辑像素和像素密度,为什么在挪动端开发时须要用到 @3x, @2x 这种图片?

以 iPhone XS 为例,当写 CSS 代码时,针对于单位 px,其宽度为 414px & 896px,也就是说当赋予一个 DIV 元素宽度为 414px,这个 DIV 就会填满手机的宽度;

而如果有一把尺子来理论测量这部手机的物理像素,理论为 1242*2688 物理像素;通过计算可知,1242/414=3,也就是说,在单边上,一个逻辑像素 = 3 个物理像素,就说这个屏幕的像素密度为 3,也就是常说的 3 倍屏。

对于图片来说,为了保障其不失真,1 个图片像素至多要对应一个物理像素,如果原始图片是 500300 像素,那么在 3 倍屏上就要放一个 1500900 像素的图片能力保障 1 个物理像素至多对应一个图片像素,能力不失真。当然,也能够针对所有屏幕,都只提供最高清图片。尽管低密度屏幕用不到那么多图片像素,而且会因为下载多余的像素造成带宽节约和下载提早,但从后果上说能保障图片在所有屏幕上都不会失真。

还能够应用 CSS 媒体查问来判断不同的像素密度,从而抉择不同的图片:

my-image {background: (low.png); }
@media only screen and (min-device-pixel-ratio: 1.5) {#my-image { background: (high.png); }
}

如何判断数组类型

Array.isArray

协商缓存和强缓存的区别

(1)强缓存

应用强缓存策略时,如果缓存资源无效,则间接应用缓存资源,不用再向服务器发动申请。

强缓存策略能够通过两种形式来设置,别离是 http 头信息中的 Expires 属性和 Cache-Control 属性。

(1)服务器通过在响应头中增加 Expires 属性,来指定资源的过期工夫。在过期工夫以内,该资源能够被缓存应用,不用再向服务器发送申请。这个工夫是一个相对工夫,它是服务器的工夫,因而可能存在这样的问题,就是客户端的工夫和服务器端的工夫不统一,或者用户能够对客户端工夫进行批改的状况,这样就可能会影响缓存命中的后果。

(2)Expires 是 http1.0 中的形式,因为它的一些毛病,在 HTTP 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更准确的管制。它有很多不同的值,

Cache-Control可设置的字段:

  • public:设置了该字段值的资源示意能够被任何对象(包含:发送申请的客户端、代理服务器等等)缓存。这个字段值不罕用,个别还是应用 max-age= 来准确管制;
  • private:设置了该字段值的资源只能被用户浏览器缓存,不容许任何代理服务器缓存。在理论开发当中,对于一些含有用户信息的 HTML,通常都要设置这个字段值,防止代理服务器 (CDN) 缓存;
  • no-cache:设置了该字段须要先和服务端确认返回的资源是否产生了变动,如果资源未发生变化,则间接应用缓存好的资源;
  • no-store:设置了该字段示意禁止任何缓存,每次都会向服务端发动新的申请,拉取最新的资源;
  • max-age=:设置缓存的最大有效期,单位为秒;
  • s-maxage=:优先级高于 max-age=,仅实用于共享缓存(CDN),优先级高于 max-age 或者 Expires 头;
  • max-stale[=]:设置了该字段表明客户端违心接管曾经过期的资源,然而不能超过给定的工夫限度。

一般来说只须要设置其中一种形式就能够实现强缓存策略,当两种形式一起应用时,Cache-Control 的优先级要高于 Expires。

no-cache 和 no-store 很容易混同:

  • no-cache 是指先要和服务器确认是否有资源更新,在进行判断。也就是说没有强缓存,然而会有协商缓存;
  • no-store 是指不应用任何缓存,每次申请都间接从服务器获取资源。

(2)协商缓存

如果命中强制缓存,咱们无需发动新的申请,间接应用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。

下面曾经说到了,命中协商缓存的条件有两个:

  • max-age=xxx 过期了
  • 值为no-store

应用协商缓存策略时,会先向服务器发送一个申请,如果资源没有产生批改,则返回一个 304 状态,让浏览器应用本地的缓存正本。如果资源产生了批改,则返回批改后的资源。

协商缓存也能够通过两种形式来设置,别离是 http 头信息中的 EtagLast-Modified 属性。

(1)服务器通过在响应头中增加 Last-Modified 属性来指出资源最初一次批改的工夫,当浏览器下一次发动申请时,会在申请头中增加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当申请发送到服务器后服务器会通过这个属性来和资源的最初一次的批改工夫来进行比拟,以此来判断资源是否做了批改。如果资源没有批改,那么返回 304 状态,让客户端应用本地的缓存。如果资源曾经被批改了,则返回批改后的资源。应用这种办法有一个毛病,就是 Last-Modified 标注的最初批改工夫只能准确到秒级,如果某些文件在 1 秒钟以内,被批改屡次的话,那么文件已将扭转了然而 Last-Modified 却没有扭转,这样会造成缓存命中的不精确。

(2)因为 Last-Modified 的这种可能产生的不准确性,http 中提供了另外一种形式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中增加了 Etag 属性,这个属性是资源生成的惟一标识符,当资源产生扭转的时候,这个值也会产生扭转。在下一次资源申请时,浏览器会在申请头中增加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接管到申请后会依据这个值来和资源以后的 Etag 的值来进行比拟,以此来判断资源是否产生扭转,是否须要返回资源。通过这种形式,比 Last-Modified 的形式更加准确。

当 Last-Modified 和 Etag 属性同时呈现的时候,Etag 的优先级更高。应用协商缓存的时候,服务器须要思考负载平衡的问题,因而多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因而在思考负载平衡时,最好不要设置 Etag 属性。

总结:

强缓存策略和协商缓存策略在缓存命中时都会间接应用本地的缓存正本,区别只在于协商缓存会向服务器发送一次申请。它们缓存不命中时,都会向服务器发送申请来获取资源。在理论的缓存机制中,强缓存策略和协商缓存策略是一起单干应用的。浏览器首先会依据申请的信息判断,强缓存是否命中,如果命中则间接应用资源。如果不命中则依据头信息向服务器发动申请,应用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器间接应用本地资源的正本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。

call/apply/bind 的实现

call

形容 :应用 一个指定的 this 值 (默认为 window) 一个或多个参数 来调用一个函数。

语法function.call(thisArg, arg1, arg2, ...)

核心思想

  • 调用 call 的可能不是函数
  • this 可能传入 null
  • 传入不固定个数的参数
  • 给对象绑定函数并调用
  • 删除绑定的函数
  • 函数可能有返回值

实现

Function.prototype.call1 = function(context, ...args) {if(typeof this !== "function") {throw new TypeError("this is not a function");
    }
    context = context || window; // 如果传入的是 null, 则指向 window
    let fn = Symbol('fn');  // 发明惟一的 key 值, 作为结构的 context 外部办法名
    context[fn] = this;  // 为 context 绑定原函数(this)
    let res = context[fn](...args); // 调用原函数并传参, 保留返回值用于 call 返回
    delete context[fn];  // 删除对象中的函数, 不能批改对象
    return res;
}

apply

形容:与 call 相似,惟一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个参数数组或类数组。

实现

Function.prototype.apply1 = function(context, arr) {if(typeof this !== "function") {throw new TypeError("this is not a function");
    }
    context = context || window; // 如果传入的是 null, 则指向 window
    let fn = Symbol('fn');  // 发明惟一的 key 值,作为结构的 context 外部办法名
    context[fn] = this;  // 为 context 绑定原函数(this)
    let res;
    // 判断是否传入的数组是否为空
    if(!arr) {res = context[fn]();}
    else {res = context[fn](...arr); // 调用原函数并传参, 保留返回值用于 call 返回
    }
    delete context[fn];  // 删除对象中的函数, 不能批改对象
    return res;
}

bind

形容bind 办法会创立一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时应用。

核心思想:

  • 调用 bind 的可能不是函数
  • bind() 除了 this 外,还可传入多个参数
  • bind() 创立的新函数可能传入多个参数
  • 新函数可能被当做结构函数调用
  • 函数可能有返回值

实现

Function.prototype.bind1 = function(context, ...args) {if (typeof that !== "function") {throw new TypeError("this is not function");
    }
    let that = this;  // 保留原函数(this)return function F(...innerArgs) {
        // 判断是否是 new 构造函数
        // 因为这里是调用的 call 办法,因而不须要判断 context 是否为空
        return that.call(this instanceof F ? this : context, ...args, ...innerArgs);
    }
}

new 实现

形容new 运算符用来创立用户自定义的对象类型的实例或者具备构造函数的内置对象的实例。

核心思想:

  • new 会产生一个新对象
  • 新对象须要可能拜访到构造函数的属性,所以须要从新指定它的原型
  • 构造函数可能会显示返回对象与根本类型的状况(以及 null)

步骤 :应用new 命令时,它前面的函数顺次执行上面的步骤:

  1. 创立一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的隐式原型 (__proto__),指向构造函数的prototype 属性。
  3. 让函数外部的 this 关键字指向这个对象。开始执行构造函数外部的代码(为这个新对象增加属性)。
  4. 判断函数的返回值类型,如果是值类型,返回创立的对象。如果是援用类型,就返回这个援用类型的对象。

实现

// 写法一:function myNew() {
    // 将 arguments 对象转为数组
    let args = [].slice.call(arguments);
    // 取出构造函数
    let constructor = args.shift();

    // 创立一个空对象,继承构造函数的 prototype 属性
    let obj = {};
    obj.__proto__ = constructor.prototype;

    // 执行构造函数并将 this 绑定到新创建的对象上
    let res = constructor.call(obj, ...args);
    // let res = constructor.apply(obj, args);

    // 判断构造函数执行返回的后果。如果返回后果是援用类型,就间接返回,否则返回 obj 对象
    return (typeof res === "object" && res !== null) ? res : obj;
}

// 写法二:constructor:构造函数,...args:结构函数参数
function myNew(constructor, ...args) {
    // 生成一个空对象, 继承构造函数的 prototype 属性
    let obj = Object.create(constructor.prototype);

    // 执行构造函数并将 this 绑定到新创建的对象上
    let res = constructor.call(obj, ...args);
    // let res = constructor.apply(obj, args);

    // 判断构造函数执行返回的后果。如果返回后果是援用类型,就间接返回,否则返回 obj 对象
    return (typeof res === "object" && res !== null) ? res : obj;
}

说一下原型链和原型链的继承吧

  • 所有一般的 [[Prototype]] 链最终都会指向内置的 Object.prototype,其蕴含了 JavaScript 中许多通用的性能
  • 为什么能创立“类”,借助一种非凡的属性:所有的函数默认都会领有一个名为 prototype 的共有且不可枚举的属性,它会指向另外一个对象,这个对象通常被称为函数的原型
function Person(name) {this.name = name;}

Person.prototype.constructor = Person
  • 在产生 new 结构函数调用时,会将创立的新对象的 [[Prototype]] 链接到 Person.prototype 指向的对象,这个机制就被称为原型链继承
  • 办法定义在原型上,属性定义在构造函数上
  • 首先要说一下 JS 原型和实例的关系:每个构造函数(constructor)都有一个原型对象(prototype),这个原型对象蕴含一个指向此构造函数的指针属性,通过 new 进行结构函数调用生成的实例,此实例蕴含一个指向原型对象的指针,也就是通过 [[Prototype]] 链接到了这个原型对象
  • 而后说一下 JS 中属性的查找:当咱们试图援用实例对象的某个属性时,是依照这样的形式去查找的,首先查找实例对象上是否有这个属性,如果没有找到,就去结构这个实例对象的构造函数的 prototype 所指向的对象下来查找,如果还找不到,就从这个 prototype 对象所指向的构造函数的 prototype 原型对象下来查找
  • 什么是原型链:这样逐级查找形似一个链条,且通过 [[Prototype]] 属性链接,所以被称为原型链
  • 什么是原型链继承,类比类的继承:当有两个构造函数 A 和 B,将一个构造函数 A 的原型对象的,通过其 [[Prototype]] 属性链接到另外一个 B 构造函数的原型对象时,这个过程被称之为原型继承。

标准答案更正确的解释

什么是原型链?

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

什么是原型继承?

一个对象能够应用另外一个对象的属性或者办法,就称之为继承。具体是通过将这个对象的原型设置为另外一个对象,这样依据原型链的规定,如果查找一个对象属性且在本身不存在时,就会查找另外一个对象,相当于一个对象能够应用另外一个对象的属性和办法了。

正文完
 0