[FLAG] 纸上得来终觉浅,绝知此事要躬行
[PS] 先立个flag准备打自己的脸
代码运行环境:Chrome 74.0.3729.169
new 运算符
在学习new运算符之前先来看一下MDN关于new运算符的介绍。
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
- 创建一个空的简单的JavaScript对象 {}
- 链接该对象(即设置该对象的构造函数)到另一个对象
- 将步骤1新创建的对象作为this的上下文
- 如果该函数没有返回对象,则返回this
写个很常见的例子
function Person(){}
var tom = new Person()
好了,这样tom就出生了。
一、首先说一下构造函数Person。(参考自《javaScript高级教程设计》第三版 P148 )
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,所有的原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针,即
Person.prototype.constructor = Person
图 1-1 |
---|
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,其他方法,则都是从Object继承而来(ps:这一点可以很清楚的从图1看到)。当调用构造函数创建一个新的实例后,该实例内部将包含一个指针,指向构造函数的原型对象。在规范中这个指针叫[[Prototype]]。实际上Chrome等浏览器都实现了一个__proto__(ps:两个下划线)。这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间
图 1-2 |
---|
我曾经一度以为prototype每个对象都会有,然后就一直处于懵逼状态。仔细观察上面引用内容所提到的prototype:为该函数创建。也就是说函数是有这个属性的,并没有提及对象的事。那么对象到底有没有呢。从图2可以看到是没有的,用in操作符试一下也显示false。看到了这里应该对如何实现new运算符的前两步有了一个大致的思路。
var tom = {}
tom.__proto__ = Person.prototype
我们简单的完成了前两步,创建一个对象, 链接该对象(即设置该对象的构造函数)到另一个对象。
如果你熟悉ECMAScript 5 中的一个新方法Object.create那么上面的代码也可以这样写(可能还是因为规范没有明确指出有__proto__这个属性):
var tom = Object.create(Person.prototype)
图 1-3-1 | 图 1-3-2 |
---|---|
二、如何将创建的对象作为this的上下文
上去就是一张图
图 2-1 |
---|
图2-1就是大概的效果。js中修改this的指向有 apply, call,bind,这个也是老生常谈的话题了,下面引用MDN的内容简单介绍下。
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
call() 允许为不同的对象分配和调用属于一个对象的函数/方法。
返回值:使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
注意:该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。
返回值:返回一个原函数的拷贝,并拥有指定的this值和初始参数。
这里以call/bind为例,上图
图 2-2-1 | 图 2-2-2 |
---|---|
我们将新创建的对象作为this的上下文,并执行了Person构造函数,又近一步。
三、如果函数没有返回对象,则返回this
尼克杨:this是谁?如果函数返回了对象又怎么办?
这些问题确认令人困惑,不妨先来看看这个步骤的英文版
- Creates a blank, plain JavaScript object;
- Links (sets the constructor of) this object to another object;
- Passes the newly created object from Step 1 as the this context;
- Returns this if the function doesn’t return its own object.
从3,4句大概能看出来this指向了context一般翻译过来也就是上下文。再看一下ecma-262-this中12.2.2.1的介绍
Runtime Semantics: Evaluation
PrimaryExpression :Return ResolveThisBinding( )
8.3.3 ResolveThisBinding ( )
The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context. ResolveThisBinding performs the following steps:
Let envRec be GetThisEnvironment( ).
Return envRec.GetThisBinding().
看完一脸懵逼的状态。大概能臆测出来,按照之前写的代码来说就是 先将创建的obj对象作为代码的运行环境上下文,然后将this指向这个上下文并返回。上图
图 3-1 |
---|
从图3-1可以看出来tom果然就是那个男人。tom和that指向的同一个内存地址。
所以第四句在前三句的基础上可以这么翻译:如果函数没有返回对象就返回第一步我们创建的对象。
然后我们来看尼克杨同学的第二个问题。话不多说,上图为敬。
图 3-2 |
---|
可以看到简直可以说是狸猫换太子,tom没了,出来一个fakerTom。第四句可以补充为:如果函数没有返回对象就返回第一步创建的对象,否则返回函数自身返回的对象。
截至目前我们所写的例子都不包含参数,下面我们来实现一下带有参数的函数。
function _new(){
var obj = {},
constructor = [].shift.call(arguments); // 将第一个参数作为构造器,后面的为参数
obj.__proto__ = constructor.prototype; // 第二步
var result = constructor.apply(obj,arguments); // 第三步
return typeof result === 'object' || typeof result === 'function' ? result : obj // 第四步
}
下面我们来测试一下
图 3-3 |
---|
关于构造函数返回function类型不再继续说明,可自行测试。
至此已基本写完,撒花撒花★,°:.☆( ̄▽ ̄)/$:.°★ 。
垂死挣扎的附加题(原型链,类似MDN示例):
function person() {}
person.prototype.age = "18";
var tom = new person();
tom.age = "12";
console.log(tom.age); // 12
console.log(person.age) // undefined
【总结】本人比较懒散,前前后后写了好几天以至于最后我也不知道我在写什么。逻辑有些混乱,请见谅。写的过程中为了尽量保持客观性查了很多资料,也是学了很多东西。还是的自己写写。
【感谢】关二哥给的精神支持(¬_¬)