zl程序教程

您现在的位置是:首页 >  后端

当前栏目

你真的了解JS里的"new"吗?

JS 了解 quot 真的 New
2023-09-14 09:12:39 时间

我们常常喜欢用new关键字去创建一些对象如new Vue(),但是这个关键字的背后究竟做了什么其实没太多人去关注。

想象我们是苹果公司,要生产30部iPod,规定:

  • 每台iPod都会有自己的ID
  • 每台iPod都是一样的制造商:Apple
  • 每台iPod的功能都是一样的(函数一样)
let iPod= {
    // 制造商不会变
    manufacturer: 'Apple',
    // 播放音乐
    play: function () { },
    // 暂停
    pause: function () { },
    // 继续播放
    resume: function () { }
}

好了,现在我们开始生产吧。

简单生产

要生产那么多iPod,那就循环30次吧。每次循环都创建一个对象,将这个对象加入到数组里就行了。

let box = []
let iPod
 
for (let i = 0; i < 30; i++) {
    iPod= {
        // 每次都改变 ID
        id: i,
        // 制造商不会变
        manufacturer: 'Apple',
        // 播放音乐
        play: function () {},
        // 暂停
        pause: function () {},
        // 继续播放
        resume: function () {}
    }
 
    box.push(iPod)
}
 
Manufacturer.deliver(box)

但是,这里有一个问题:每次都会新创建play()pause()resume()这些函数,manufacturer的值也是一样,没必要重新建。

使用原型改进

使用原理链,我们可以将上面说到的共有函数,属性放在一个共有对象里,然后用iPod.__proto__指向这个iPodCommon不就好了吗?所以现在代码可以改写成这样:

let box = []
let iPod
 
let iPodCommon = {
    // 制造商不会变
    manufacturer: 'Apple',
    // 播放音乐
    play: function () {},
    // 暂停
    pause: function () {},
    // 继续播放
    resume: function () {}
}
 
for (let i = 0; i < 30; i++) {
    iPod= {
        // 每次都改变 ID
        id: i,
    }
 
    iPod.__proto__ = iPodCommon
 
    box.push(iPod)
}
 
Manufacturer.deliver(box)

这样就好多了,省了很多空间。但是这个iPod对象的代码有点太分散了,跟for循环耦合在一起了。学习重构时听得最多的一句就是重复代码最好用函数包起来,所以我们可以试着传入要改变的属性(ID)用函数来返回iPod对象。,

函数返回对象

我们可以用一个函数返回iPod对象,这样就不用每次都在for循环里去定义对象了。

function iPod(id) {
    let tempObj = { }
    // 自有属性
    tempObj.id = id
    // 共有属性,函数
    tempObj.__proto__ = iPod.common
 
    return tempObj
}
 
iPod.common = {
    // 制造商不会变
    manufacturer: 'Apple',
    // 播放音乐
    play: function () {},
    // 暂停
    pause: function () {},
    // 继续播放
    resume: function () {}
}
// 保存为 iPod.js 文件

然后在创建时候引入这个文件,再去生成iPod。

let box = []
 
for (let i = 0; i < 30; i++) {
    box.push(iPod(i))
}
 
Manufacturer.deliver(box)

有没有感觉这样清爽了很多?我们将所有有关iPod的逻辑都放在一个文件里,这样就和主文件完全解耦了。

new

上面是很清爽,但是每次都要写创建一个临时对象好麻烦。这时候JS的new就上场了,它的作用如下:

  1. 帮你创建临时对象tempObj,函数里的this绑定为这个tempObj
  2. 统一共有属性所在对象的名字叫prototype而不是comon
  3. 帮你完成原型的绑定
  4. 帮你返回临时对象tempObj
    现在iPod.js文件可以写成这样
function iPod(id) {
    this.id = id
}
 
// 共有属性
iPod.prototype = {
    // 制造商不会变
    manufacturer: 'Apple',
    // 播放音乐
    play: function () {},
    // 暂停
    pause: function () {},
    // 继续播放
    resume: function () {}
}
// 保存为 iPod.js 文件

使用new再次生产iPod

let box = []
 
for (let i = 0; i < 30; i++) {
    box.push(new iPod(i))
}
 
Manufacturer.deliver(box)

这就是new的由来,不过是一种语法糖,和Java里面的new是完全不一样的东西,希望大家不要混为一谈。当然了,最后的这个iPod函数也就成了我们所说的构造函数

 

Js中万物皆对象。

 

js里的对象,属性等说法,其实就是指针,它们指向自己的实例空间(除了基本类型)

 

先看一个简单的function变量

 

function fun1(name) {

            this.name = name;

}

 

console.log("fun1", fun1)

 

 

 

 

 从结果可以看到定义一个function,它里边所含有的内容这六个属性是每个function所必有的,直接看第五个prototype(注意prototype是一个对象)就是传说中的原型(本文只称它为prototype),第六个属性是灰色的并且用尖括号括起来,它这么不显眼是因为js就不想让程序员去用它,在以前版本的浏览器它有另外一个名字叫__proto__(为了方便区分,下文就以__proto__来称呼它,而且在以前的版本里名字是__proto__,以下划线开头还是两个说明是js坚决拒绝程序员去修改它的,但是下面为了剖析其内部原理我会对其做一些粗暴的改变,大家注意在工程中尽量避免)。

 

如果大家去实验一下就会发现,每个对象都会有__proto__这个属性,但一般情况下只有声明function的变量(例如上图中的fun1)才会有(自动生成)prototype这个属性,而function通过在它的名字前加new 可以创建出属于它的实例,因此我认为js里的function有三个角色:函数,对象和类(类似于Java里的对象可以通过类产生,有人说js和java没有半毛钱关系,我想说它们都是面向对象的语言,都有对象这个概念,那么伴随的也就有类,function声明的变量就是类)。而prototype这个就体现了function类的概念

 

 

 

可以看到在 prototype里有两个属性constructor和__proto__,在前面我们说过prototype是一个对象和每个对象都会有__proto__这个属性,因此prototype也是有__proto__这个属性;constructor(构造方法)这个属性是在生成prototype时自动生成的属性,其指向函数本身(在申明函数时,js自动创建该函数的peototype属性)。

 

function fun1(name) {

            this.name = name;

}

 

console.log("fun1", fun1)

console.log(fun1.prototype.constructor === fun1)

 

 

一个对象__proto__指向产生它的类的prototype(就是指向new 是后边所跟的那个东西,严格来讲是它作为左值时等号右边的东西,function fun1(){}等价于var fun1 = function(){},fun1的__proto__指向Function的prototype)。

 

function fun1(name) {

            this.name = name;

}

 

var temp = new fun1("");

var obj = new Object;

console.log(temp.__proto__ === fun1.prototype)

console.log(fun1.__proto__ === Function.prototype)

console.log(obj.__proto__ === Object.prototype)

 

 

总结一下:

 

所有对象都有__proto__属性,是用来通过__proto__找到它的原型即prototype,function声明的变量的__proto__指向Function的prototype,其它对象的__proto__指向Object的prototype

function声明的变量、Function和Object都有prototype, 有prototype的东西可以产生实例(即可以作为new 后边的值)。 

prototype它是一个对象(在声明函数变量是在函数内部由js自动创建),因此它也有__proto__,并且指向Object的prototype。

 

 

Function和Object非常特殊,我们来具体分析

 

首先看一下Function里的东西

 

console.log("Function", Function);

console.log("Function.__proto__", Function.__proto__)

 

 

 从控制台的打印可以明显的看出Function的__proto__指向了它自己的prototype

 

 

 

接下里看Object(Object里有很多其它的东西,为了简洁我直接打印Object的prototype)

 

console.log("Object.prototype", Object.prototype)

console.log("Object.__proto__", Object.__proto__)

 

 

Object的prototype和Function的prototype的__proro__指向是相同的如下图:

 

 

 

 

 

综上可以看出Object的__proto__指向Function的prototype,而Object的prototype并没有灰色的<prototype>即__proto__,即它是一切之源。

 

console.log("Object.prototype.__proto__", Object.prototype.__proto__)

 

 

 我将Object的prototype称为源型,下面我给出我个人对这些现象的解释:

 

源型是js的内置的一段代码,所有所有通过这个源型创造出的都是object,第一步先创造出Function的prototype,因此这个prototype的__proto__指向源型,然后再通过这个prototype造出Function,因此Function的__proto__指向它自己的prototype,然后用Function造出Object,因此Object的__proto__指向Function的prototype,然后js直接将Object的prototype替换为源型。

 

并且我认为js里判断继承(即A instanceof B)是沿着__proto__往下走,首先要求B必须有prototype属性且必须是一个对象(否则会浏览器会报 'prototype' property of B is not an object),判断时先判断A的__proto__是否和B的prototype指向是否相同(即===结果为true),若相同则返回true,若不相同则判断A的__proto__指向属性里是否还有__proto__属性,若有则进行再次进行判断指向是否相同,直到找到源型,它的__protot__为null,返回false

 

为了证明只要A的__protot__和B的prototype指向相同就返回true,给了如下测试:

 

var obj = new Object;

function fun () {};

console.log(obj instanceof fun);

var temp = new Object

fun.prototype = temp

obj.__proto__ = temp;

console.log(obj instanceof fun)

 

 

有点颠覆三观不过习惯就好。

 

下面用我的结论来解释下边四个现象:

 

console.log(Function instanceof Function)

console.log(Function instanceof Object)

console.log(Object instanceof Function)

console.log(Object instanceof Object)

 

 

Function的__proto__和Function的prototype指向相同,因此返回true,

Function的__proto__和Function的prototype指向指向相同,Function的prototype的__protot__和Object的prototype指向相同,因此返回true。

Object的__proto__和Function的prototype指向相同(因为Object就是以Function为模板创造的),因此返回true。

Object的__proto__指向Function的prototype,Function的prototype的__proto__指向Object的prototype,这个prototype是属于Object(饶了一圈),因此返回true。

只要高清内部原理,理解instanceof就非常简单

 

下面再来一个小测试:

 

var obj = new Object;

obj.__proto__ = Function.prototype;

console.log(obj instanceof Function)

 

 

 

 

总结:peototype是原型,__proto__所指向的以及其后的所有peototype称为原型链。“js里一切皆对象”倒不如所是js里的所有对象都是由“源型”生成。

 

 

 

简单(较优雅的方式)实现继承

 

在上篇博文里讲过function声明的对象(比如fun)有三个角色:函数、对象和类,作为类js会自动给它添加prototype属性,prototype是一个对象,它里面第一个属性就是constructor(构造函数),按道理说通过它(fun)创建的实例时有三步(这和java的new的过程很像)

 

在内存里分配好所需的空间

执行构造函数,完成对成员的初始化

将空间的首地址赋值给等号有边的变量

但问题是在第二步的时候,执行的构造函数是prototype里的constructor吗

 

function animal(name) {

            this.name = name;

}

 

animal.prototype.constructor = null;

 

console.log("animal", animal)

var a = new animal("狗子")

console.log("a", a)

 

 

我将animal的prototype的constructor属性赋值为null,但是a依然被正确的构造出来了,并且a的<prototype>也就是__proto__中的constructor也是null,我认为在构造实例的时候js调用构造函数时是直接调用animal本身的代码,而不是通过animal的prototype访问其constructor。那么constructor是不是就没有用了呢?在这里我觉得prototype里的constructor属性只是给一个对象提供找到它的够造函数来用的,给它一个追根溯源的机会(可能我见识浅薄暂时看不到它的用处)。

 

什么是继承:继承要实现的目的就是子类的对象可以调用父类提供的一些公共的属性和方法,子类对象是通过__proto__来寻找的。当子类对象要访问某一个方法或属性,js会先在子类对象里寻找是否有该属性若没有则沿着__proto__指向的prototype空间找,找到了则返回,子类对象只能访问而不能修改,若要试图给父类的属性赋值,则js只会在该对象创建一个名称一样的变量而不会修改父类的prototype空间里的值。

 

那么如上文所说直接修改类的prototype的__protot__属性,让它指向另一个类的prototype不就可以实现继承吗,但我们坚决拒绝直接修改__proto__。先讲一下思路:首先我们需要让一个类的prototype的__proto__指向另一个类的prototype,但是不能直接修改prototype的__proto__属性,但我们可以修改该类的prototype属性,也就是我们可以创建一个对象,让该对象的__proto__指向父类的prototype并且让这个对象作为子类的prototype。

 

具体做法:先声明一个function变量,让其的prototype指向父类的prototype,并new 它,产生的对象的__proto__就指向了父类的prototype,然后将它作为子类的prototype就完成了继承(那种直接new 父类并将产生的对象作为子类的方法是不可取的,因为在new父类时会调用父类的构造函数生成很多垃圾属性,这些属性不应该存在在prototype中)

 

function ExtendClass(base, klass) {

            if (base == undefined ||  base == null || klass == undefined || klass == null || !base instanceof Object || !klass instanceof Function) {

                        return;

            }

 

            function fun() {};

            fun.prototype = base.prototype;

            klass.prototype = new fun();

            klass.prototype.constructor = klass;

}

 

function Anaimal(name) {

            this.name = name;

}

 

function Dog(name, type) {

            Anaimal.call(this, name)

            this.type = type;

}

ExtendClass(Anaimal, Dog);

 

var dog = new Dog("小白", "京巴");

console.log("Anaimal", Anaimal)

console.log("Dog", Dog)

console.log("dog", dog)

 

 

我编写了一个继承的函数,继承函数必须在类变量声明后在prototype修改前调用,否则以前的修改无效。

 

从上图可以得知我们成功让Dog继承Animal,即让Dog的prototype的__proto__指向Animal的prototype属性。这时dog就可以调用它的原型链里面的所有方法和属性。

 

一般在new的过程中我们是先调用父类的构造方法,然后依次执行,所以我们在new Dog时要在内部先调用Animal的构造方法(因为哪个类继承与哪个类是程序员所知道的,因此可以直接指定调用哪个方法作为父类构造方法),但是如果你在Dog代码块内部直接调用Animal(“”)方法它的默认执行对象是window,即最终的结果不是给dog里增加一个name属性,而是用Animal类创建了一个匿名对象,因此使用call函数,它的第一个参数是执行的对象,之后是形参,我将this传进去就相当于我将执行对象变为dog,这样dog就成功执行了父类的构造方法。

————————————————

版权声明:本文为CSDN博主「Backee」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/backee/article/details/83378772