JS 进阶知识点

手写 call、apply 及 bind 函数
  • 不传入第一个参数,那么上下文默认为 window。
  • 改变了 this 指向,让新的对象可以执行该函数,并能接受参数。
  • call的实现
//context 为可选参数,如果不传的话默认上下文为 window
Function.prototype.myCall = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
//给 context 创建一个 fn 属性,并将值设置为需要调用的函数
context.fn = this
const args = [...arguments].slice(1)
// call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
const result = context.fn(...args)
//调用函数并将对象上的函数删除
delete context.fn
return result
}
  • apply的实现
//context 为可选参数,如果不传的话默认上下文为 window
Function.prototype.myCall = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
//给 context 创建一个 fn 属性,并将值设置为需要调用的函数
context.fn = this
let result
//处理参数和 call 有区别
if (arguments[1]){
result = context.fn(...arguments[1])
}else{
result = context.fn()
}
//调用函数并将对象上的函数删除
delete context.fn
return result
}
  • bind的实现
//context 为可选参数,如果不传的话默认上下文为 window
Function.prototype.myCall = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
const _this = this
const args = [...arguments].slice(1)
// 返回一个函数
/*
bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式
*/
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
/*
通过 new 的方式调用:对于 new 的情况来说,不会被任何方式改变 this ,所以对于这种情况我们需要忽略传入的 this
*/
return new _this(...args, ...arguments)
}
/*
直接调用:选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2) ,所以我们需要将两边的参数拼接起来,于是就有了这样的实现:
args.concat(...arguments)
*/
return _this.apply(context, args.concat(...arguments))
}
}
new
  • 在调用 new 的过程中会发生以上四件事情:
  • 1.新生成了一个对象
  • 2.链接到原型
  • 3.绑定 this
  • 4.返回新对象
  • 自己实现一个new
function create() {
//创建一个空对象
let obj = {}
//获取构造函数
let Con = [].shift.call(arguments)
//设置空对象的原型
obj.__proto__ = Con.prototype
//绑定 this 并执行构造函数
let result = Con.apply(obj, arguments)
//确保返回值为对象
return result instanceof Object ? result : obj
}
  • 与字面量创建对象方法(推荐)的区别:使用 new Object()的方式创建对象需要通过作用域链一层层找到 Object ,使用字面量的方式就没这个问题。
function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()
instanceof 的原理
  • instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的prototype 。
function myInstanceof(left, right) {
//获取类型的原型
let prototype = right.prototype
//获得对象的原型
left = left.__proto__
while (true) {
//一直循环判断对象的原型是否等于类型的原型,直到对象原型为null ,因为原型链最终为 null
if (left === null || left === undefined)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
0.1 + 0.2 != 0.3
  • 循环的数字被裁剪了,就会出现精度丢失的问题。
  • console.log(0.1) 是正确的:为在输入内容的时候,二进制被转换为了十进制,十进制又被转换为了字符串,在这个转换的过程中发生了取近似值的过程,所以打印出来的其实是一个近似值。
  • 解决办法:​​parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true​
垃圾回收机制(分代式)
  • 新生代算法(对象一般存活时间较短)
  • 内存空间分为两部分,分别为 From 空间和 To 空间:有一个空间是使用的,另一个空间是空闲的。

新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将From 空间和 To 空间互换,这样 GC 就结束了

  • 老生代算法(对象一般存活时间较长且数量也多)
  • 标记清除算法

在老生代中,以下情况会先启动标记清除算法:

  • 某一个空间没有分块的时候
  • 空间中被对象超过一定限制
  • 空间不能保证新生代中的对象移动到老生代中

遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。

  • 标记压缩算法

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象像
一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

  • 老生代算法中的对象
  • 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
  • To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。