首页 > 分享发现 > javascript 里的 new 操作和继承(全记录)

javascript 里的 new 操作和继承(全记录)

主要是记录下 js 里关于 new 和 继承的细节,因为网上的资料都比较分散,这里汇总下,并且加上了思考的过程。

new 运算创建实例对象

javascript 里使用构造函数和 new 运算符创建实例对象的过程如下:


function Parent(inNumber,inArr,inFn){// 一个构造函数

  this.value = inNumber || 1;

  this.arr = inArr || [1,2,3];

  this.fn = inFN || function(){console.log("默认fn函数")}

}

var child = new Parent;// 通过构造函数创建实例

console.log(child) // {value:1,arr:[1,2,3],fn:f}

 

按照 MDN 的说法,上面的 new Parent 这一过程中,程序主要做了以下工作:

  1. A new object is created, inheriting from Parent.prototype.// 创建了一个新的空对象,这个对象继承自 Parent.prototype
  2. The constructor function Parent is called with the specified arguments, and with this bound to the newly created object. new Parent is equivalent to new Parent(), i.e. if no argument list is specified, Parent is called without arguments.// 接下来调用 Parent,函数执行过程中 this 被设定为新创建的那个空对象,如果没有指定参数的话 new parentnew parent() 是一样的效果
  3. The object returned by the constructor function becomes the result of the whole new expression. If the constructor function doesn't explicitly return an object, the object created in step 1 is used instead. (Normally constructors don't return a value, but they can choose to do so if they want to override the normal object creation process.)//函数执行过程结束后,如果没有显式的 renturn 语句,那个参与执行过程的新对象就会被作为结果返回出来。(一般来说,构造函数不需要显示的返回值,不过如果需要的话也可以使用 renturn 值覆盖掉默认创建的对象作为返回结果)

用代码表示就是


var newProcess = function(){

  var tmpObj = {};

  tmpObj.value = inNumber || 1;

  tmpObj.arr = inArr || [1,2,3];

  tmpObj.fn = inFn || function(){console.log("默认函数~")};

  return tmpObj

}

var childNode = newProcess();

 

可以看到,每进行一次 new 操作,都会开辟一个新的空对象,因此每个实例之间的属性都不会互相影响。如下:


var childNodeOne = new ParentNode;

var childNodeTwo = new ParentNode;


console.log("%c childNodeOne.arr:","color:green",childNodeOne.arr) // [1,2,3]

console.log("%c childNodeTwo.arr:","color:orange",childNodeTwo.arr) // [1,2,3]

console.log("%c 是否相等:","color:red",childNodeOne.arr === childNodeTwo.arr) // false

继承

看看上面英文解释的第一句: inheriting from Parent.prototype . 这就是 js 里实现继承的关键。

 

prototype

Parent.prototype 这个式子通用的写法是:constructor.prototype. constructor 表示构造函数,js 里每个构造函数都有对应的一个原型对象,即 prototype,在定义一个函数的时候,这个prototype 对象自动产生,并且会带有一个 constructor 属性指向刚刚创建的那个函数,关系可以看作如下这样:

prototype

要确立一个观念 函数的 prototype 属性是一个对象!

那么 inheriting(继承)又是怎么产生的呢?结论就是:把新创建对象的 [[prototype]] 指向构造函数的 prototype(对象),这样继承关系就建立了。举个例子:


function A(){

  this.value = 1;

  this.arr = [1,2,3];

  this.fn = function(){console.log("默认")};

}

var B = new A;

上面这段代码,直观的展示如下:

继承2

那个最后创建的空对象赋值给 B 就构成了我们的实例对象。

因此可以看出, new 的过程中,程序为我们实现了继承关系的创立。这其中又牵涉到一个叫做 [[prototype]] 的东西

做了个视频演示这个过程:

 

[[prototype]]

上面提到一个新的概念 [[prototype]] ,这是 ECMAScript 标准里规定的一个内部值,js 里除了 null,每个对象都有这样一个值(之前提到的 prototype 是每个构造函数都有),而js 里一切皆对象,因此这个 [[prototype]] 就普遍存在啦。

 

网上很多描述 js 的内容都会提到 __proto__ 这样一个属性, __proto__ 是一个浏览器用来访问内部 [[prototype]] 值的访问器属性,也就是 __proto__[[prototype]] 的一个代号。__proto__ 是浏览器自己的一个实现,老的 ECMAScript 标准中并没有这样一个概念,不过为了兼容浏览器, ECMAScript 2015 把 __proto__ 这个概念加到了标准中来,但是原则上是不建议使用的,要访问那个内部的 [[prototype]] 值,标准建议使用 Object.getPrototypeOf 方法,要设置这个内部值可以使用 Object.setPrototypeOf 方法。

 

继承就是设置对象的 [[prototype]] 过程,即:Object.setPrototypeOf(B,A)B[[prototype]] 指向 A 对象。非标准写法就是 B.__proto__ = A.prototype(标准中不推荐这么写)

 

继承实际举例

假设我们希望创建一个名为 hello 的小狗实例,然后这个实例继承自狗这个大类,然后狗这个大类又继承自 Animal 这个大类。

那么首先我们来创建 Animal 这个大类。这可以用一个 Animal 构造函数来表示


function Animal(inputType,inputWeight){

  this.method = function(){console.log("动物的通用方法")};

  this.legs = 4;

  this.weight = inputWeight || "未标明体重";

  this.type = inputType || "没指定类别";

  this.extraInfo = [1,2,3]

}

 

然后我们要创建狗这样一种动物。下面的 Dog 构造函数就表示创建狗这个大类,这为接下来创建狗的实例做基础。


function Dog(inputName){

  this.name = inputName || "没有名称";

}

 

最后我们实现某个狗的实例,例如 aDog 狗,那么一定会是如下形式:


var aDog = new Dog("hello");

 

到此,创建的那个aDog 实例实现了对 Dog 大类的继承,但是怎么让 aDog 也继承 Animal ,即用到 Animal 上的方法呢?

显然我们需要把这个狗的大类和动物这个大类关联起来,即让狗大类继承 Animal 这个大类。

综合前面的描述,new 的过程实现了对象之间的继承,于是我们可以利用这一点来达到我们的目的:

new Animal()这个过程会产生一个继承自 Animal 的对象;

我们把那个对象指定到 Dog.prototype 属性上不就表示 Dog.prototype 继承自 Animal 了么,即:

Dog.prototype = new Animal();

相当于把原来的 Dog.prototype 的值替换成了 Animal 的一个实例;

 

分析整个思路后,实际实现的过程就如下:


function Animal(inputType,inputWeight){// 父级构造函数 Animal

  this.method = function(){console.log("动物的通用方法")};

  this.legs = 4;

  this.weight = inputWeight || "未标明体重";

  this.type = inputType || "没指定类别";

  this.extraInfo = [1,2,3]

}


function Dog(inputName){// 次一级构造函数 Dog

  this.name = inputName || "没有名称";

}


Dog.prototype = new Animal("狗"); // 首先让 Dog.prototype 实现对 Animal 的继承,这一步一定要在具体实现实例之前完成


var aDog = new Dog("hello"); // 然后创建一个 继承自 Dog 的实例

Dog.prototype instanceof Animal // true

aDog instanceof Dog // 返回 true

aDog instanceof Animal // 返回 true

如此一来 aDog 就实现了 继承 Dog 和 Animal 的过程.

 

使用这种方式实现继承有一定的问题:所有通过 Dog 构造函数构建的实例,如果没有明确指定自己的 extraInfo 属性值,那么都会继承 this.extraInfo=[1,2,3] 这里的属性值。当某个实例中对这个 extraInfo 做改动后,所有实例上的这个值都会变化,代码如下:


//前面的构造函数以及继承方式的书写和之前相同

var aDog = new Dog("hello");

var bDog = new Dog("b-hello");


console.log(aDog.extraInfo); // [1, 2, 3]

console.log(bDog.extraInfo); // [1, 2, 3]

console.log(aDog.extraInfo === bDog.extraInfo); // true



aDog.extraInfo.push("test");// 通过 aDog 实例修改 extraInfo 属性



console.log(aDog.extraInfo);// [1, 2, 3,"test"]

console.log(bDog.extraInfo);// [1, 2, 3,"test"] bDog 上的属性也跟着变了,这不是我们所希望的

console.log(aDog.extraInfo === bDog.extraInfo);// true

 

为了规避上面的问题,我们在 new Dog("hello") 过程中,可以为每个创建的实例都创建一份属于自己的属性,这期间也可实现子类自由的属性,如下:


//Animal 的实现还是和上面一样

function Dog(inputName,inputType,inputWeight){

  this.name = inputName || "没有名称";

  this.method = function(){console.log("动物的通用方法")};
  this.legs = 4;

  this.weight = inputWeight || "未标明体重";

  this.type = inputType || "没指定类别";

  this.extraInfo = [1,2,3]

}


Dog.prototype = new Animal(); // 首先让 Dog.prototype 实现对 Animal 的继承,这一步一定要在具体实现实例之前完成



var aDog = new Dog("hello");

var bDog = new Dog("b-hello");



console.log(aDog.extraInfo); // [1, 2, 3]

console.log(bDog.extraInfo); // [1, 2, 3]

console.log(aDog.extraInfo === bDog.extraInfo); // false



aDog.extraInfo.push("test");



console.log(aDog.extraInfo);// [1, 2, 3,"test"]

console.log(bDog.extraInfo);// [1, 2, 3] bDog 没有再跟着变了

console.log(aDog.extraInfo === bDog.extraInfo);// false


console.log(bDog.method.toString === aDog.method.toString) // true

console.log(aDog.method === bDog.method) // false

可以看出,如此一来各个实例之间的属性确实是互不干扰了,不过呢同样的方法(method)却也得不到复用了,显然有点浪费空间,而且这样来实现的话相当于把 Animal 的过程完全重写了一遍,那么 Animal 就没存在的意义了。下面就针对这两点做一些改进:

  1. 如何使相同的方法得到复用;
  2. 如何复用上一级的处理过程;

 

为了实现上面的目的,思路如下:

  1. 把这个需要复用的方法挂载到构造函数的原型对象上,这样就可以达到 实例复用同一个方法
  2. 复用处理过程,也就是在下一级的构造函数中再次执行父一级的函数过程

 

思路想好了,那么改写如下:


function Animal(inputType,inputWeight){

  this.legs = 4;

  this.weight = inputWeight || "未标明体重";

  this.type = inputType || "没指定类别";

  this.extraInfo = [1,2,3]

}


Animal.prototype.method = function(){console.log("动物的通用方法")}; // 为了继承后实例上的方法能够复用,把通用的方法写到 父类的 原型上去



function Dog(inputName,inputType,inputWeight){

  this.name = inputName || "没有名称";

  Animal.call(this,inputType,inputWeight); // 复用上一级的处理过程,这里用 call 主要是为了实现构造函数的 this 传递

}


Dog.prototype = new Animal("狗"); // 首先让 Dog.prototype 实现对 Animal 的继承,这一步一定要在具体实现实例之前完成


var aDog = new Dog("hello");

var bDog = new Dog("b-hello");



console.log(aDog.extraInfo); // [1, 2, 3]

console.log(bDog.extraInfo); // [1, 2, 3]

console.log(aDog.extraInfo === bDog.extraInfo); // false


aDog.extraInfo.push("test");


console.log(aDog.extraInfo);// [1, 2, 3,"test"]

console.log(bDog.extraInfo);// [1, 2, 3] bDog 没有再跟着变了

console.log(aDog.extraInfo === bDog.extraInfo);// false


console.log(bDog.method.toString === aDog.method.toString) // true

console.log(aDog.method === bDog.method) // true , 公用的方法得到了复用

通过不断的改良,发现至此最后实例的继承基本满足需求了。

当然还有一点小小的瑕疵:

  1. 首先是查询 aDog 实例的构造函数会发现是:Animal
  2. 执行这个语句主要目的在于把 Animal 的一个实例传递给 Dog.prototype ,然后实现 Dog 实例对于 Animal 的继承,但是在传递过程中实例上的属性也被一并给过来了

这两点小瑕疵反应在代码上如下:


console.log(aDog.constructor) // function Animal(){...}

console.log(Dog.prototype) // {legs: 4, weight: "未标明体重", type: "狗", extraInfo: Array(3)} 这个作为传递作用的原型上含有一份默认的属性

 

constructor 那个问题好解决,怎么处理那些实例上的属性呢?下面是网上常用的方法,引入一个中间构造函数,单独用来传递父一级的 prototype


function Animal(inputType,inputWeight){

  this.legs = 4;

  this.weight = inputWeight || "未标明体重";

  this.type = inputType || "没指定类别";

  this.extraInfo = [1,2,3]

}


Animal.prototype.method = function(){console.log("动物的通用方法")}; // 为了继承后实例上的方法能够复用,把通用的方法写到 父类的 原型上去


function Dog(inputName,inputType,inputWeight){

  this.name = inputName || "没有名称";

  Animal.call(this,inputType,inputWeight); // 复用上一级的处理过程,这里用 call 主要是为了实现构造函数的 this 传递

}


function middleFunc(){}; // 创建一个没有执行过程的中间函数,用来单独继承 父一级的 prototype;

middleFunc.prototype = Animal.prototype;


Dog.prototype = new middleFunc(); // 首先让 Dog.prototype 实现对 Animal 的继承,这一步一定要在具体实现实例之前完成

middleFunc.prototype.constructor = Dog; //修改构造函数的指向


var aDog = new Dog("hello");

var bDog = new Dog("b-hello");



console.log(aDog.extraInfo); // [1, 2, 3]

console.log(bDog.extraInfo); // [1, 2, 3]

console.log(aDog.extraInfo === bDog.extraInfo); // false


aDog.extraInfo.push("test");



console.log(aDog.extraInfo);// [1, 2, 3,"test"]

console.log(bDog.extraInfo);// [1, 2, 3] bDog 没有再跟着变了

console.log(aDog.extraInfo === bDog.extraInfo);// false

console.log(bDog.method.toString === aDog.method.toString) // true

console.log(aDog.method === bDog.method) // true , 公用的方法得到了复用


console.log(aDog.constructor) // function Dog(){...}

console.log(Dog.prototype) // {constructor:Dog} 那些多余的属性都没有啦~

至此所有的小毛病都解除啦,but!让我们回头看看第一个粗糙的继承。

 

上面提到要实现 aDog 对 Animal 的继承,只要让 Dog.prototype 实现对 Animal 的继承就好啦。

之前我们用的方式是,Dog.prototype = new Animal();

相当于是用一个已经继承了 Animal 的实例替换了原本的 Dog.prototype,通过这种方式来实现了连续的继承。那如果不用中间实例呢?也就是 Dog.prototype = Animal.prototype

 

由于 Animal.prototype 是一个对象(引用数据类型),像上面这样传递的是一个地址而已,如果后期对Dog.prototype做修改的话会影响到父一级,类似下面的简化例子:


var a = {1:1,2:2}

var b = {a:"a"}

b = a // a对象传递给b


console.log(b) // {1: 1, 2: 2}

console.log( a === b); // true


b.c = "test"

console.log(a) // {1:1,2:2,c:"test"} 对b的修改影响到了 a

 

ES5里的 Object.create

上面最终实现的版本中有这么一些过程:


function middleFunc(){}; // 创建空函数

middleFunc.prototype = Animal.prototype; // 空函数的 prototype 指向父一级

Dog.prototype = new middleFunc(); // 构建一个继承父级的空对象,然后覆盖掉Dog.prototype

 

然后这个过程我们可以封装一下


function inherit(parentObj){

  var middleFunc = function(){};

  middleFunc.prototype = parentObj;

  return new middleFunc;

}

Dog.prototype = inherit(Animal.prototype)

 

在ES5 里头,这种继承关系可以通过 Object.create 来实现:


Dog.prototype = Object.create (Animal.prototype)

关于这个 Object.create,可参见:MDN Object.create();

 

ES6里的 Object.setPrototypeOf

再思考下,按照之前的说法,要让 B 对象继承 A,就是要让 B [[prototype]] 指向 A.prptotype 的即可,上面都是覆盖 B.prototype 的方式来实现的,我们试试来直接改变 [[prototype]] 指向来实现继承,如下:


function Animal(inputType,inputWeight){

  this.legs = 4;

  this.weight = inputWeight || "未标明体重";

  this.type = inputType || "没指定类别";

  this.extraInfo = [1,2,3]

}


Animal.prototype.method=function(){console.log("公用方法")}


function Dog(inputName,inputType,inputWeight){

  this.name = inputName || "没有名称";

  Animal.call(this,inputType,inputWeight); // 复用上一级的处理过程,这里用 call 主要是为了实现构造函数的 this 传递

}


Object.setPrototypeOf(Dog.prototype,Animal.prototype); //ES 6里引入了这个setPrototypeOf,等效于 Dog.prototype.__proto__ = Animal.prototype;


var newDog = new Dog("hello");

var newDog2 = new Dog("hello2");



newDog.extraInfo.push("test")

console.log(newDog.name) //hello

console.log(newDog2.name) // hello2

console.log(newDog.extraInfo) //[1, 2, 3, "test"]

console.log(newDog2.extraInfo) // [1, 2, 3]


console.log(newDog.method === newDog2.method) // true


console.log(newDog instanceof Dog) // true

console.log(newDog instanceof Animal) // true

console.log(newDog.constructor) // Dog()

结论是,可以!并且这种方式,对于 Dog.prototype 是不需要重写 constructor 的。

关于这个 Object.setPrototypeOf,可参见:MDN Object.setPrototypeOf()

 

留言板 当前主题:0

留言审核后可见.

相关杂记
快递查询插件--快递管家(支持国际件,自动提醒)

双十一到了,剁手族们的购物车内肯定囤积了很多的待购商品,可以想象之后快递员又有的忙了。这里放出一个自制的浏览器插件

阅读更多>>
javascript 里的 new 操作和继承(全记录)

主要是记录下 js 里关于 new 和 继承的细节,因为网上的资料都比较分散,这里汇总下,并且加上了思考的过程。

阅读更多>>
使用CDN来加载js等文件

博客里常常会带有一些代码演示的内容,这时候就希望代码里的关键词能够高亮显示。

阅读更多>>
DOS快餐店连载系列下载

这是很早之前《电脑爱好者》杂志上连载的内容,一共12期,主要是以小故事的形式讲解了 windows 下命令行工具的使用。例如批量重命名,循环等。

阅读更多>>
ionic3 自定义图标文件(亲测可用)

首先准备好用来做图标的 svg 文件,你可以自己用 ai 画或者去网上下载就行啦,如下图所示:

阅读更多>>