Javascript天下第一 !
面试中经常会被问到,new方法实现的原理,你能不能实现一个,在这个框架泛滥的年代,我还是决定沉下心来,自己在把基础知识过一过,自己造一些轮子,沉淀沉淀。
说到new,call,bind的方法,就不得不从原型链开始说起
想起最早接触原型链的时候,看起来特别头疼,特别绕,后来也因为工作,经常写业务代码,渐渐的对这些基础就没怎么用,而且用的也是很普通的给构造函数上添加一个个的方法,更深曾的就没去思考,去实现。
最近学习东西的时候喜欢带着一些问题去学习,去实现自己脑中的想法,然后去带着问题去解决问题,接下来就先介绍下原型链的一些基础知识,帮助大家理解下,可以更好的去实现这些内置方法。介绍方法前,先把一些经常会被问到的问题列出来,去学习的时候想着这些问题,去问问题,然后才能解决问题。
- 如何准确判断一个变量是数组
- 写一个原型链继承的例子
- 描述一下new一个对象的过程,手动实现一个(今天的主题)
问题挺多的,先写这3个问题,接下来简单介绍下原型链
这里我称 __proto__ 为隐式原型,prototype称为显示原型,方便读,方便交流
原型规则
所有的引用类型(数组,对象,函数),都具有对象特性:可自由扩展属性
所有的引用类型(数组,对象,函数),都有一个__ proto__属性,属性值是一个普通对象
所有的函数,都有一个prototype属性,属性值是一个普通的对象
所有的引用类型(数组,对象,函数),__ proto__属性指向它的构造函数的prototype属性值
当试图得到一个引用类型(数组,对象,函数)的某个属性时,如果这个对象本身并没有这个属性,那么会去它的__ proto__(也就是其构造函数的prototype)上去查找
原型链大侄介绍这么多,更多原型链的这里就不赘述了。
new的实现
接下来我们来实现new方法,要实现一个方法,我们先思考这个方法做了什么事情,有输入吗,有输出(返回)吗,需要传参吗,参数类型有要求吗,等...
我们来分析下,new的时候都做了什么事情
- 首先,我们会发现new完之后我们会得到一个对象,所以,new的时候会新创建一个空对象,并且最后会把我们创建好的对象给我们的变量,也就变成了一个实例了。
let obj = { };
- 然后我们发现,这个new之后生成的对象(实例),它具有构造函数的所有方法以及属性。所以此时new操作做的事情就是把构造函数上的所有属性以及方法都赋值给咱们这个新创建的对象。
obj.__proto__ = fn.prototype fn指的是构造函数
- 想下前面的操作,我们不是都拿到了构造函数上面所有的方法了吗,但是此时如果直接将这个新对象返回,我们得到的仅仅是一个包含构造函数原型链上方法集合的一个对象。还缺少了当前构造函数内部的属性,所以需要把当前的this值指向新的对象,来拿到构造函数里的所有属性,当然,别忘了传参数。
fn.apply(obj, arguments);
- 最后一步,将这个新的对象返回
return obj
总结
所以整合一下上面的思路,简单来写就是4步:
1)创建一个新对象
2)新对象的隐式原型链上挂载构造函数的显示原型链上的方法
3)把this指针指向新的对象
4)把对象返回
完整代码
function _new() {
// 缓存一个arguments,避免直接修改污染arguments
let _arguments = arguments;
// 拿到构造函数,shift处理之后_arguments值已经更改,shift会对原始数据有影响
let fn = Array.prototype.shift.call(_arguments)
// 第一步,创建一个对象
let target = {};
// 第二步,链接到原型,给新对象的隐式原型赋予构造函数的显示原型,得到构造函数的所有prototype属性值
target.__proto__ = fn.prototype;
// 第三步,把当前的this指针指向新的对象
fn.apply(target, _arguments);
// 第四步,返回当前this
return target;
}
call的实现
同样的道理,我们先分析call做了什么事情
首先看下mdn上面对call的使用,它的语法是:
fun.call(thisArg, arg1, arg2, ...)
参数:call方法可以接受很多参数,其中第一个参为在 fun 函数运行时指定的 this 值,后面的是参数列表
作用:call方法提供新的 this 值给当前调用的函数/方法
返回值:使用调用者提供的 this 值和参数调用该函数的返回值,如果该函数没有返回值,则返回undefined,有返回值则返回值为该值
好了,清楚了call的使用方法和作用,咱们来看看它具体是怎么做到的:
- 首先,上面提到了this,参数,被调用函数,所以我们应该拿到这些数值
let _this = Array.prototype.shift.call(arguments)
//这里将拿到传过来的第一个参数,并且改变了当前的参数列表,这里拿到的是this值
//由于[].shift操作会改变原来的数组,所以当前arguments剩下的值就是参数列表,不过是一个类数组
如何拿到被调用的函数呢,这里有两个写法,不过只是用法不同而已
fn._call(this)
//如果是这么调用的话,要获得被调用函数,那么就是当前的this了,因为目前的_call方法是会挂到Function.prototype上
_call(fn,this)
//如果这么调用的话,就是取参数列表里面的第二个参数值了,看自己怎么传参了~
这里我采用第一种写法,毕竟emmm,比较方便
- 现在所需要的东西都有了,this,参数,被调的函数,那么剩下的就很简单了,仔细想下call做了什么,不就是用新的指针去调用被调函数吗,所以,看代码:
_this.fn = this;
//_this是上面从参数列表里面拿到了的新的this指针
//this是当前的方法,也就是被调用的函数
//这段代码的意思是把需要被调用的函数挂载到新的指针下面
- 接下来就很简单了,去执行新的方法就好了,不过记得这里有返回值,所以直接返回这个函数执行的结果就好哦~
return _this.fn(...arguments);
完整代码
Function.prototype._call = function () {
// 不管什么操作,先缓存arguments,避免污染
let _arguments = arguments;
// 如果第一个参数是一个null,那么target为全局对象
// 拿到新的this对象,剩余的都是参数
let _this = Array.prototype.shift.call(_arguments);
if (_this == null) target = window;
// 拿到需要call的方法
// 第一步,给当前的target下面添加所需要的方法
_this.fn = this;
// 第二步,把参数传给target下面的函数
return _this.fn(...arguments);
}
//测试代码
var foo = {
name: '张三'
}
function info(job, age) {
console.log('name', this.name)
console.log(job, age)
}
var name = 'window 张三'
info._call(foo,'web developer',24)
info._call(null,'web developer',24)
bind的实现
暂且下一期实现