js原型链+继承+this new call apply的使用及模拟实现
前言
本篇是阅读了很多业界大佬的博客后,结合自己的一点感悟写成的。方便自己后续进行复习和巩固。大家可以按照以下推荐的阅读顺序进行初步学习,其中可能会踩的坑,读不懂的地方,我都会在下文做出解释。如果熟练掌握以下的部分,原型链相关的一连串知识就足够在面试中造火箭了!
感谢所有博客的作者,受益匪浅!
<推荐按以下顺序阅读>
- https://github.com/mqyqingfeng/Blog/issues/2 初步了解从原型到原型链的基本内容
- https://juejin.cn/post/6844903496253177863?share_token=2938a268-c236-489a-a760-01195c54b8cb#heading-0 看懂this指向问题和call apply bind
- https://juejin.cn/post/6844903476766441479 学习new的模拟实现
- https://juejin.cn/post/6844903476477034510#heading-0 学习apply和call的模拟实现
- https://juejin.cn/post/6844903575974313992#heading-0 深入解析原型链的各大难点
- https://juejin.cn/post/6844903477819211784#heading-4 多种继承汇总及其优缺点
1.从原型到原型链中需要注意的一些细节(第一篇)
-
从实例到其原型除了使用.__proto__外还有一种object.getPrototypeOf(实例) 即:
实例对象.__proto__ === object.getPrototypeOf(实例对象)
所以更推荐使用object.getPrototypeOf(实例)的方法,这是一个存在于原型的原型(object.prototype)中的一个属性,__proto__也只是他的getter/setter -
constructor
constructor是每个实例的原型默认生成的属性,存在于原型对象中。所以不仅原型可以.constructor指向构造函数,实例也可以使用.constructor指向构造函数。(因为调用时发现构造函数中不存在这个属性便会向他的原型中查找)person.constructor === Person.prototype.constructor
-
真的是继承嘛?
2.1.this的指向(第二篇)
有两个词需要划重点:最后和调用
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name); // Cherry
}
}
window.a.fn();
最后调用的仍然是a;
var name = "windowsName";
var a = {
name : null,
// name: "Cherry",
fn : function () {
console.log(this.name); // windowsName
}
}
var f = a.fn;
f();
f=a.fn的操作只是完成了赋值,将fn函数赋值给f。真正发生调用的地方是在f(),此时是被window全局对象调用的。
-
运用箭头函数 :因为箭头函数不会重新绑定this指向;在函数创建时,箭头函数中的this已经确定,并非执行时。
var name = "windowsName"; var a = { name : "Cherry", func1: function () { console.log(this.name) }, func2: function () { setTimeout( () => { this.func1() },100); } }; a.func2() // Cherry
-
使用_this = this;
将需要的this指向保存在一个新的变量中,下文使用该变量即可。 -
使用apply call bind重新绑定this指向
fun.apply(obj,[n1,n2]); fun.call(obj,n1,n2); fun.bind(obj,n1,n2)();
apply和call的写法一样,唯一区别是给函数的传参方式;
bind()是创建了一个新的函数,需要手动去调用;
- 作为一个函数调用(声明在全局,调用也是全局,this为全局)
- 函数作为方法调用(挂载在对象中,通过对象.调用,this指向调用者)
- 使用构造函数调用函数(new)
如果函数前使用了new,则调用了构造函数。这起来似乎是创建了一个新的函数,其实创建了一个新的对象
ps:这个地方new的原理看不懂很正常 接着看下一篇博客 会具体讲到new的代码实现
3.new的代码实现(第三篇):
new bind call apply的模拟实现都分为两个阶段:
1.基本功能的实现
2.一些特殊情况的补充
我们先要思考这个函数需要实现哪些功能,初步实现并进行测试;之后考虑一些细节部分进行函数扩充;
- 一句话介绍new
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一
- new需要实现的功能
1.需要返回一个对象
2.访问构造函数中的属性
3.访问构造函数的原型中的属性
(4.细节处理:
—如果构造函数中return一个对象,则只能给新建的object返回对象中的值
—如果构造函数中return一个基本类型,则该返回什么就返回什么)
第四点返回值的效果展示
//构造函数返回一个对象
function Otaku (name, age) {
this.strength = 60;
this.age = age;
return {
name: name,
habit: 'Games'
}
}
var person = new Otaku('Kevin', '18');
console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // undefined
console.log(person.age) // undefined
//构造函数返回一个基本类型
function Otaku (name, age) {
this.strength = 60;
this.age = age;
return 'handsome boy';
}
var person = new Otaku('Kevin', '18');
console.log(person.name) // undefined
console.log(person.habit) // undefined
console.log(person.strength) // 60
console.log(person.age) // 18
- 代码实现
function Animal (name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.type = "fly";
//用new创建一个实例对象
//var bee = new Animal("Ben",16);
//通过手写的create创建实例对象
var bee = create(Animal,"Ben",16);
//create函数 (参数可省略,因为个数未知)
function create(){
//创建一个obj空对象
var obj = new Object();
//获取到构造函数
var constructor = [].shift.call(arguments);
//实现:访问原型中的属性
obj.__proto__ = constructor.prototype;
//实现:访问构造函数的属性 并且根据构造函数的返回值确定最后return的值
var res = constructor.apply(obj,arguments);
return typeof res === 'Object'? res: obj;
}
- [ ].shift.call(arguments);这一步是什么意思?
作用:把arguments类数组对象转化成数组对象,删除并拿到第一项构造函数。
- 原理:arguments是个类数组对象,无法直接使用数组函数。而数组函数shift内部实现是使用的this代表调用对象。那么当[].shift.call() 传入 arguments对象的时候,通过 call函数改变原来 shift方法的this指向, 使其指向arguments,并对arguments进行复制操作,而后返回一个新数组。至此便是完成了arguments类数组转为数组的目的!
一篇关于 这个原理的解释
浅谈arguments与arguments的妙用
4. apply和call的代码实现(第四篇)
-
一句话介绍call
call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
-
call()需要实现的功能
用bar.call(foo)举例:
将bar函数中的this指向修改为foo对象 ——》可以近似理解为将bar函数添加到foo对象中
于是为我们的模拟过程提供了一条思路:
1.将函数设为对象的属性 ---------- foo.fn = bar
2.运行该函数(将参数传入)------foo.fn()
3.删除对象中的该函数---------------delete foo.fn
(4.细节处理:
First — call的第一个参数this可能是null,此时函数的this指向是全局window;
Second — 函数是可以有返回值的!)第四点的细节展示
//this指向全局window var value = 1; function bar() { console.log(this.value); } bar.call(null); // 1 -------------------------------------------- //函数存在返回值 var obj = { value: 1 } function bar(name, age) { return { value: this.value, name: name, age: age } } console.log(bar.call(obj, 'kevin', 18)); // Object { // value: 1, // name: 'kevin', // age: 18 // }
-
call代码实现
var foo = { value = 1 } function bar(name,age){ console.log(this.value) return{ name:name; age:age } } //使用call的写法 bar.call(foo,"Ben",16); //调用自己手写的call2 bar.call2(foo,"Ben",16); //call2函数 Function.prototype.call2 = function(context){ //当传入的this为null时,指向window全局 var context = context||window; //将函数设为对象属性 context.fn = this. //获取到arguments中传入的形参 var arr=[]; for(let i=1;i<arguments.length;i++){ arr.push("arguments["+i+"]") } //传入参数,运行该函数 var res = eval('context.fn('+arr+')'); //删除对象中的函数 delete context.fn; //处理函数返回值问题 return res; }
- eval(‘context.fn(’+arr+’)’) 中eval的作用?
在回答第二个问题时,我们首先要理解eval()的作用。
mdn对eval()函数的解释是:
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码
所以我们将函数的执行以字符串的方式传入,其中的arr数组会自动调用Array.toString()方法转换成一个字符串,以此完成函数的拼接。
mdn上对自动调用toString()的解释:
当一个数组被作为文本值或者进行字符串连接操作时,将会自动调用其 toString 方法。- 代码中arr.push(“arguments[”+i+"]") 选择将形参以【“arguments[1]”,“arguments[2]”】字符串的形式组成数组,而不是直接获取值以【“Ben”,16】组成数组传入函数的原因?
这里要结合上一个问题一起考虑,因为我们取出参数的目的是为了传递给函数运行。所以之后拼接进函数的参数类型必须和原参数类型保持一致。
由此可以发现Array.toString()函数会将数组中的值都转换为纯字符串
[“kevin”,18]-------》“kevin,18” 显然这并不是函数想要的参数
正确的参数应该是’ “kevin”,18 ’
由此,原因应该是显而易见了。 -
同理,apply的代码实现:
Function.prototype.apply = function (context, arr) { var context = Object(context) || window; context.fn = this; var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push('arr[' + i + ']'); } result = eval('context.fn(' + args + ')') } delete context.fn return result; }
5. 再次回顾原型链中的各大难点(第五篇)
有了之前new call apply的知识积累,这时候看原型链又会有新的感悟
-
原型链中常用属性的所处位置总结
1.prototype存在于构造函数中:当function foo(){}创建了一个函数时,会自动创建一个prototype属性,但他是一个内部属性,实例是无法使用的。这个属性指向一个对象,就是原型,原型中又自动生成一个constructor。
2.__proto__也就是object.getPrototypeOf() 存在于object的原型对象中
3.constructor存在于构造函数的原型中:原型都自动生成一个constructor -
通过第一个问题,我们可以进一步延伸,解释new需要做的事情:
prototype和constructor都会在创建函数的时候自动生成,使构造函数和原型对象中间产生联系。那么如何在实例和原型之间产生联系的呢,也就是说__proto__是如何创建的呢?
答案是:我们在new一个实例的过程中,为这个对象添加了一个__proto__属性
我们知道__proto__是全局对象object原型中的属性,所以我们在new一个实例对象时进行了下面的操作:function create() { let obj = new Object() let Con = [].shift.call(arguments) // 这一步操作链接到原型 obj.__proto__ = Con.prototype let result = Con.apply(obj, arguments) return typeof result === 'object' ? result : obj }
-
function其实是个语法糖:
function Foo() {} // 这一部操作其实是创建了Function的实例对象Foo // function 就是一个语法糖 // 内部调用了 new Function(...)
-
既然语法糖function Foo()是调用了new Function(),生成了一个Function的实例对象。是不是可以说明函数也有一个构造对象,一切函数都是由这个构造对象创造的,其中的运行机制是如何 ?
这里先放一个原型链的总结图,这个问题的解答要从这个图说起
来,我们一点点拆分这个图,我们会发现一些无法用以前的知识解释的地方:
在这个图中,我们发现Function.__proto__ = Function.prototype
这难道说明Function自己产生了自己?之前我们知道原型对象只是在构造函数被new的时候自动生成的,那么这里的Function是哪里来的?
所以我们可以得出一个结论,不是所有函数都是new Function()
产生的。有了Function.prototype
以后才有了function Function()
,然后其他的构造函数都是function Function()
生成的。为什么
Function.__proto__
会等于Function.prototype
,个人的理解是:其他所有的构造函数都可以通过原型链找到Function.prototype
,并且function Function()
本质也是一个函数,为了不产生混乱就将function Function()
的__proto__
联系到了Function.prototype
上。
我们可以做出如下关系图:
-
总结
1.Object.prototype
是所有对象的爸爸,所有对象都可以通过__proto__
找到它(包括函数Function.prototype
,因为函数本质也是一个对象)
2.Function.prototype
是所有函数的爸爸,所有函数都可以通过__proto__
找到它
3.Function.prototype
和Object.prototype
是两个特殊的对象,他们由引擎来创建
4. 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
5. 函数的prototype
是一个对象,也就是原型
6. 对象的__proto__
指向原型,__proto__
将对象和原型连接起来组成了原型链
6.js中的继承(第六篇)
最后一节了!我自己都觉得这篇博客内容太过冗长了,不过仔细看一遍肯定会有收获的,加油,马上就结束啦!!