ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
### 手写call、apply和bind函数 **call、apply和bind函数内部实现是怎么样的?** 考虑: * 不传第一个参数,上下文默认为window * 改变了this指向,让新的对象可以执行该函数,并能接受参数 1. call的实现 ~~~ Function.prototype.myCall = function(context){ if(typeof this !== 'function'){ throw new TypeError('error') } context = context || window; context.fn = this; const args = [...arguments].slice(1); const result = context.fn(...args); delete context.fn; return result; } ~~~ 分析 * 首先context为可选参数,如果不传的话默认上下文为window * 接下来给context创建一个fn属性,并将值设置为需要调用的函数 * 因为call可以传入对个参数作为调用函数的参数,所以需要将参数剥离出来 * 然后调用函数并将对象上的函数删除 ~~~ Function.prototype.myCall = function(context){ console.log(context);//传入的对象Food{} console.log(this);//this指调用myCall的函数对象(Product) if(typeof this !== 'function'){//this必须是函数 throw new TypeError('error') } context = context || window; //若没有传入参数,默认传入的对象是window context.fn = this; console.log(context)//Foo{fn:f} console.log(context.fn)//为传入对象添加属性,属性名fn,属性值是函数this(调用myCall的函数) const args = [...arguments].slice(1); //序列化其他参数 console.log(args) //["chees",5]; //...args 解构 ('cheese',5) const result = context.fn(...args);//调用对象上的方法,修改context对象的属性,得到Food{name:'cheese',price:5} console.log(result);//函数没有返回值,所以是undefined delete context.fn; //将对象添加的方法属性删除 return result;//undefined } function Product(name, price) { this.name = name; this.price = price; } function Food(name, price) { //this 指实例化对象Food{} console.log(this); //将参数(实例化对象,参数属性值)传入myCall Product.myCall(this, name, price); console.log(this);//Food{name:'cheese',price:5} this.category = 'food'; console.log(this);//Food{name:'cheese',price:5,category:'food'} } console.log(new Food('cheese', 5).name); ~~~ 精简 ~~~ ~~~ Function.prototype.myCall = function(context){ if(typeof this !== 'function'){//this必须是函数 throw new TypeError('error') } context = context || window; //若没有传入参数,默认传入的对象是window context.fn = this; const args = [...arguments].slice(1); //序列化其他参数 const result = context.fn(...args);//调用对象上的方法,修改context对象的属性,得到Food{name:'cheese',price:5} delete context.fn; //将对象添加的方法属性删除 return result;//undefined } function Product(name, price) { this.name = name; this.price = price; } function Food(name, price) { Product.myCall(this, name, price); this.category = 'food'; } console.log(new Food('cheese', 5).name); ~~~ ~~~ 2. apply的实现类似,区别在于对参数的处理 ~~~ Function.prototype.myApply = function(context){ console.log(context);//传入的对象Food{} console.log(this);//this指调用myCall的函数对象(Product) if(typeof this !== 'function'){//this必须是函数 throw new TypeError('error') } context = context || window; //若没有传入参数,默认传入的对象是window context.fn = this; console.log(context)//Foo{fn:f} console.log(context.fn)//为传入对象添加属性,属性名fn,属性值是函数this(调用myCall的函数) console.log(arguments); let result; if(arguments[1]){ result = context.fn(...args);//调用对象上的方法,修改context对象的属性,得到Food{name:'cheese',price:5} }else{ result = context.fn(); } delete context.fn; //将对象添加的方法属性删除 return result;//undefined } function Product(name, price) { this.name = name; this.price = price; } function Food(name, price) { //this 指实例化对象Food{} console.log(this); //将参数(实例化对象,参数属性值)传入myCall Product.myApply(this, [name, price]); console.log(this);//Food{name:'cheese',price:5} this.category = 'food'; console.log(this);//Food{name:'cheese',price:5,category:'food'} } console.log(new Food('cheese', 5).name); ~~~ 3. bind实略微复杂了一点。因为bind需要返回一个函数,需要判断一些边界问题 bind()方法创建一个新的函数,在调用时设置this关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数系列的前若干项。 ~~~ Function.prototype.myBind = function(context){ if(typeof this !== 'function'){ throw new TypeError('error') } const _this = this;//调用的函数 //[...arguments] //argument:[{x:42,getX:f},3,4] //...arguments{x:42,getX:f} 3, 4 //[...arguments] [{x:42,getX:f},3,4] //[...arguments].slice(1)) 获取除了第一个参数外的所有参数[3,4] const args = [...arguments].slice(1) //返回一个函数 return function F(){ //因为返回了一个函数,我们可以new F(),所以需要判断 if(this instanceof F){ return new _this(...args, ...arguments); } //args[3,4] arguments[6] return _this.apply(context,args.concat(...arguments))//context为需要绑定的对象 } } var module = { x: 42, getX: function() { console.log(this); return this.x; } } var unboundGetX = module.getX; console.log(unboundGetX());//undefined this指向window var boundGetX = unboundGetX.myBind(module,3,4); console.log(boundGetX(6));//42 var text = new boundGetX(); console.log(text)//getX类对象 console.log(text.getX) ~~~ 分析: * bind返回一个函数,对于函数来说有两种方式调用,**一种直接调用,一种通过new方式** * 直接调用:选择了apply的方式实现,对于参数需要注意,因为bind可以实现类似`f.bind(obj,1)(2)`,所以需要将两边的参数拼接起来,于是有了`args.concat(...argumets)` * **new方式,不会被任何方式改变this**,所以对于这种请求我们需要忽略传入的this ### new **new的原理是什么?通过new的方式创建对象和通过字面量创建有什么区别?** 在调用new的过程中会发生以下四件事情 1. 新生成了一个对象 2. 链接到原型(该空对象的原型指向构造函数)(将构造函数的prototype赋值给对象的`__proto__`) 3. 绑定this(将对象作为构造函数的this传进去,并执行该构造函数) 4. 返回新对象(如果构造函数返回的是一个对象,则返回该对象,否则返回第一步创建的对象) 试着自己实现一个new ~~~ function create (){ //1 let obj = new Object() //创建一个空对象 let obj = {}; //获取构造函数 let Con = [].shift.call(arguments); //2 改空对象的原型指向构造函数 obj.__proto__ = Con.prototype; //3. 将空对象作为构造函数的this传进去,并执行构造函数 let result = Con.apply(obj,arguments); //4 返回对象,如果构造函数返回的是一个对象,则返回对象,否则(没有返回或返回基础类型),返回第一步中新创建的对象。 return result instanceof Object ? result: obj; } var Person = function(name) { this.name = name console.log('name is ', this.name) } Person.prototype.getName = function() { console.log(this.name) } var createObj = create(Person,"guopei"); createObj.getName(); ~~~ `[].shift.call(arguments)`:arguments对象调用数组的shift()方法,shift()方法会删除并返回数组的第一个元素 分析: * 创建一个空对象 * 获取**构造函数** * 设置空对象的原型 * 绑定this并执行构造函数 * 确保返回值为对象 ### call、apply,bind调用的区别 ~~~ var a = {value :1}; function getValue(name, age){ console.log('arguments in fn = ', arguments) console.log(name, age) console.log(this.value) } getValue.call(a,'guo',17); getValue.apply(a,['guo',18]); let bindFn = getValue.bind(a,'textBind',45) bindFn(); ~~~ 对于对象来说,其实都是通过new产生的,无论是**function Foo()**还是 **let a= {b:1}** 创建对象:推荐使用字面量的方式创建对象(性能,可读性),如果使用`new Object()`,创建的对象需要通过作用域链一层层找到Object ~~~ function Foo() {} // function 就是个语法糖 // 内部等同于 new Function() let a = { b: 1 } // 这个字面量内部也是使用了 new Object() ~~~ ### instanceof 原理 **instanceof原理是什么?** instanceof可以正确判断对象的类型,因为**内部机制是通过判断对象的原型链中是不是能找到类型的prototype** ~~~ function myInstanceof(left, right){ let prototype = right.prototype; left = left.__proto__; while(true){ if(left === null || left == undefined){ return false; } if(prototype === left){ return true; } left = left.__proto__; } } ~~~ 分析: 1. 首先获取类型的原型 2. 然后获取对象的原型 3. 然后一直循环判断对象的原型是否等于类型的原型,知道对象原型为null,因为原型链最终为null ### 为什么 0。1+0.1 !=0.3 因为JS采用IEEE双精度版本(64位),并且只要采用IEEE754的语言都有这种问题 计算机是通过二进制存储东西的,0.1 0.1在二进制中是无限循环的一些数字,很多十进制小数用二进制表示都是无限循环的。但是JS采用的浮点数标准却会剪裁调我们的数字 这些循环的数字被剪裁了,就会出现精度丢失的问题。也造成了0.1不在是0.1 0.100000000000000002 ~~~ 0.100000000000000002 === 0.1 // true 0.200000000000000002 === 0.2 // true 0.1 + 0.2 === 0.30000000000000004 // true console.log(0.100000000000000002) // 0.1 ~~~ 既然`0.1`不是`0.1`,那为什么`console.log(0.1)`却是正确的呢? 输入内容时,二进制被转换为十进制,十进制又被转换为字符串,转换过程中发生了取近似值的过程。 解决问题: ~~~ parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true ~~~ ### 垃圾回收机制 V8 实现了准确式 GC,GC 算法采用了**分代式垃圾回收机制**。因此,V8 将**内存(堆)**分为**新生代和老生代**两部分。 #### 新生代算法 #### 老生代算法 待补