Skip to content

Latest commit

 

History

History
876 lines (657 loc) · 38.7 KB

创建对象的方式.md

File metadata and controls

876 lines (657 loc) · 38.7 KB

面向对象的概念:

  • 对象:JS中万物皆对象,它是一个泛指,(类似生活中的世界)
  • 类:对象的具体的细分,(类似生活中的人类,动物类)
  • 实例:某一个类别中具体的一个事物(类似人的个体)

转换成生活中的比喻就是:对象是一个抽象的概念,类似于我们的自然界,世界这种概念;我们自然界中分为了 人类、动物类、植物类...,而我们每一个人都是人类中的一个实例

JS中有一个内置的类 Array(数组类),每一个数组都是这个类的一个实例,我们拿出一个数组(拿出一个实例)来进行研究,研究出来的知识,我们可以说所有的数组都具备这些知识

内置类:

  • Number,String,Boolean,Null,Undefined
  • Object、Array、RegExp、Date、Function...
  • HTMLCollection(元素集合类),通过getElementsByTagName等获取的元素集合都是它的一个实例
  • NodeList(节点集合类),通过getElementsByName等获取的节点集合都是它的一个实例
  • HTMLDivElement(每一个HTML标签都有一个自己对应的类) HTMLElement Element Node EventTarget ...

创建对象的方式

创建对象的方式,最基本的有5种模式:单例模式工厂模式构造函数模式原型模式构造函数+原型模式;

其它的就是在原型模式,或者构造函数模式的基础上再次改造的模式;

  • 知识点一:单例模式
  • 知识点二:工厂模式
  • 知识点三:构造函数模式
  • 知识点四:原型模式
  • 知识点五:构造函数+原型模式
  • 知识点六:其它扩展模式
    • 知识点六.1:动态原型模式(利用原型的动态性)
    • 知识点六.2:寄生构造函数模式(构造函数模式+工厂模式)

知识点一:单例模式

单例模式:就是对象类型; Object构造函数或对象字面量都可以用来创建单个对象,下面是字面量的单例模式;(原生的Object构造函创建对象的方式,在某种意义上也是属于单例模式)

var person1 = {
  name: "朱",
  age: 18,
  say:function(){
    console.log("我是person1的say方法");
  }
};
var person2 = {
  name: "安",
  age: 28,
  say:function(){
    console.log("我是person2的say方法");
  }
};
console.dir(person1);//person1是一个单例模式
console.dir(person2);//person2是一个单例模式

把描述同一个事物(同一个对象)的属性和方法放在一个内存空间下,起到了分组的作用,这样不同事物之间的属性即使属性名相同,相互也不会发生冲突

我们把这种分组编写代码的模式叫做"单例模式"

在单例模式中我们把person1或者person2也叫做"命名空间"

单例模式,因为简单易用的原因,是封模块和日常写代码中,最常用的创建方式;

单例模式的优点

直观,易用,对象的可塑性非常强;

单例模式用在模块化开发中

单例模式是一种项目开发中经常使用的模式,项目中使用单例模式来进行"模块化"开发也是很常见的;

"模块化":对于一个相对来说比较大的项目,需要多人协作的开发的,我们一般情况下会根据当前项目的需求划分成几个功能版块,每个人负责一部分,同时开发,在需要用到的某个功能的时候,直接引用该模块即可;简单,易用,拆卸方便;

//公共模块
var utils = {
  select: function () {
    console.log("utils.select");
  }
};

//pageUtilitly的一个主文件
var pageUtilitly = {
    init:function(){
      console.log("现在开始运行bind和select中的代码");
      this.bind();//运行绑定相关的方法;
      this.select();//运行选择相关的方法,如果暂时不需要,直接注释本行代码即可
    },
    bind: function () {
      var self=this;
      self.clickEven();//在自己的命名空间下调用自己命名空间的方法
    },
    select:function(){
      utils.select();//在自己的命名空间下调用其他命名空间的方法
    },
    clickEven:function(){
        console.log("pageUtilitly.clickEven");
    }
};
pageUtilitly.init();
单例模式的缺点

面向对象,创建对象,有点类似做衣服,单例模式的优点是可定制性非常强,有点类似手工做衣服;虽然可塑性非常强,但是缺点就是如果批量做衣服的时候,效率就非常差;如下,tShirt1和tShirt2里面的方法和属性可以随意的改变,可塑性非常强;

var tShirt1 = {
    name: "t-shirt-1",
    size: "xl",
    color: "红",
    hasEffect: function () {
        console.log("这件T恤:" + this.name + "," + this.size + "," + this.color +",自带装逼效果!");
    }
};
var tShirt2 = {
    name: "t-shirt-2",
    size: "xxxl",
    color: "黑",
    hasEffect: function () {
        console.log("这件T恤:" + this.name + "," + this.size + "," + this.color +",自带撩妹效果!");
    }
};

tShirt1.hasEffect();
tShirt2.hasEffect();

单例模式虽然解决了分组和创造对象的作用,但是不能实现批量的生产,属于手工作业模式; 假设我要100件tShirt2这种自带撩妹效果的T恤,就需要重复写100次;var tShirt2 这里的代码 这就引申到另外的一种模式:"工厂模式"

知识点二:工厂模式

虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

工厂模式是软件工程领域一种广为人知的设计模式,考虑到在 ECMAScript 中无法创建类,开发人员就发明了一种函数,用函数来封装特定接口的对象,

工厂模式:说白了就是一种函数类型;

  • 把实现同一件事情的相同的代码放到一个函数中,以后如果在想实现这个功能,不需要从新的编写这些代码来了,只需要执行当前的函数即可
  • "函数的封装" -->"高内聚低耦合":减少页面中的冗余代码,提高代码的重复利用率;(工厂模式的优点就是函数模式的特点)

如下面的例子所示。

function createTshirt(name, size, color){
    var o = new Object();
    o.name = name;
    o.size = size;
    o.color = color;
    o.sayName = function(){
        console.log("这件T恤:" + this.name + "," + this.size + "," + this.color +",自带撩妹效果!");
    };
    return o;
}
var tShirt1 = createTshirt("tShirt1", "xl", "红");
var tShirt2 = createTshirt("tShirt2", "xxxl", "黑");
var tShirt3 = createTshirt("tShirt3", "xxxl", "黑");
var tShirt4 = createTshirt("tShirt4", "xxxl", "黑");

函数 createTshirt() 能够根据接受的参数来构建一个包含所有必要信息的 T恤 对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题;

JS是一门轻量级的脚本"编程语言"(HTML+CSS不属于编程语言,属于标记语言)

.net C# php Java c c++ vb vf object-c ....;所有的编程语言都是面向对象开发的->类的继承、封装、多态

继承:子类继承父类中的属性和方法 多态:当前方法的多种形态->后台语言中:多态包含重载和重写

“JS中不存在重载”,方法名一样的话,后面的会把前面的覆盖掉,最后只保留一个 "JS中有一个操作类似重载但是不是重载":我们可以根据传递参数的不一样的,实现不同的功能,属于函数的多态性;

function sum(num) {
    if (typeof num === "undefined") {
        console.log("您没有传参啊,请仔细检查");
        return;
    }
    console.log(num);
}
sum(100);
sum();

#####工厂模式的缺点;

工厂模式虽然解决了重复创建相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型),也就是某个对象属于什么类。随着 JavaScript的发展,又一个新模式出现了。

知识点三:构造函数模式

构造函数模式的意义:构造函数模式的目的就是为了创建一个自定义类,并且创建这个类的实例

function CreateJsPerson(name, age) {
    //浏览器默认创建的对象就是我们的实例p1 -> this
    this.name = name;//->变量p1实例化的时候,那么this就是p1,相当于p1.name=name
    this.age = age;
    this.writeJs = function () {
        console.log("my name is " + this.name + ",i can write js 啦!!");
    };
    //浏览器在把创建的实例默认的进行返回,无需return
}
var p1 = new CreateJsPerson("zhu", 18);//CreateJsPerson->this p1  用的时候需要new一下;创造一个构造函数的实例;
var p2 = new CreateJsPerson("an", 28);
p1.writeJs();//writeJs->this p1
console.log(p1);//顺着原型,可以看到他的__proto__指向CreateJsPerson这个构造函数;	

注意:用的时候需要new一下;创造一个构造函数的实例;如果不写new,上面的函数没有任何意义

错误的写法

var res = CreateJsPerson("zhu", 17);//这样写不是构造函数模式执行而是普通的函数执行 由于没有写return所以res=undefined  并且CreateJsPerson这个方法中的this是window
console.log(res);

构造函数模式和工厂模式的区别

  • 1、没有显式地创建对象;
  • 2、直接将属性和方法赋给了 this 对象;
  • 3、没有 return 语句。
  • 4、执行的时候,需要写new
    • 普通函数执行->createJsPerson()
    • 构造函数模式->new CreateJsPerson() 通过new执行后,我们的CreateJsPerson就是一个类了,而函数执行的返回值(p1)就是CreateJsPerson这个类的一个实例
  • 5、在函数代码执行的时候
    • 相同:都是形成一个私有的作用域,然后 形参赋值->预解释->代码从上到下执行 (类和普通函数一样,它也有普通的一面)
    • 不同:在代码执行之前,不用自己在手动的创建obj对象了(无显示过程),浏览器会默认的创建一个对象数据类型的值(这个对象其实就是我们当前类的一个实例)
    • 接下来代码从上到下执行,以当前的实例为执行的主体(this代表的就是当前的实例),然后分别的把属性名和属性值赋值给当前的实例
    • 最后浏览器会默认的把创建的实例返回
  • 6、构造函数模式的函数,推荐首字母大写;
    • 按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他 OO 语言,主要是为了区别于 ECMAScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。

要创建 CreateJsPerson 的新实例,必须使用 new 操作符

会经历四个步骤;

  • (1) 创建一个新对象;
  • (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  • (3) 执行构造函数中的代码(为这个新对象添加属性);
  • (4) 返回新对象。

在前面例子的最后, p1 和 p2 分别保存着 CreateJsPerson 的一个不同的实例。这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person ,如下所示

var p1 = new CreateJsPerson("zhu", 18);
var p2 = new CreateJsPerson("an", 28);

console.log(p1.constructor == CreateJsPerson); //true
console.log(p1.constructor == CreateJsPerson); //true

原理图如下

下面三种的对比都是true;

console.log(p1.constructor == CreateJsPerson); //true
console.log(p1.constructor == CreateJsPerson); //true

console.log(p1.__proto__.constructor == CreateJsPerson); //true
console.log(p1.__proto__.constructor == CreateJsPerson); //true

console.log(p1.__proto__ == CreateJsPerson.prototype); //true
console.log(p1.__proto__ == CreateJsPerson.prototype); //true
构造函数模式的优点:解决了工厂模式中的归属问题;

对象的 constructor 属性最初是用来标识对象类型的。但是,提到检测对象类型,还是 instanceof 操作符要更可靠一些。我们在这个例子中创建的所有对象既是 Object 的实例,同时也是 CreateJsPerson,这一点通过 instanceof 操作符可以得到验证。

console.log(p1 instanceof CreateJsPerson); //true
console.log(p1.instanceof Object); //true
console.log(p2 instanceof CreateJsPerson); //true
console.log(p2.instanceof Object); //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。在这个例子中, p1 和 p2 之所以同时是 Object 的实例,是因为所有对象均继承自 Object 。

构造函数与其他函数的区别

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。

function CreateJsPerson(name, age) {
    //浏览器默认创建的对象就是我们的实例p1 -> this
    this.name = name;//->变量p1实例化的时候,那么this就是p1,相当于p1.name=name
    this.age = age;
    this.writeJs = function () {
        console.log("my name is 【" + this.name + "】,i can write js 啦!!");
    };
    //浏览器在把创建的实例默认的进行返回,无需return
}

// 当作构造函数使用
var p1 = new CreateJsPerson("zhu", 18);
var p2 = new CreateJsPerson("an", 28);
p1.writeJs(); //"Nicholas"

// 作为普通函数调用
CreateJsPerson("hehe", 27); // 属性和方法都被添加给 window对象了
window.writeJs(); //"hehe"

// 在另一个对象的作用域中调用
var o = new Object();
CreateJsPerson.call(o, "haha", 25);
o.writeJs(); //"haha"

当在全局作用域中调用一个函数时, this 对象总是指向 Global 对象(在浏览器中就是 window 对象)。因此,在调用完函数之后,可以通过 window 对象来调用 writeJs() 方法,并且还返回了 "hehe" 。最后,也可以使用 call() (或者 apply() )在某个特殊对象的作用域中调用 CreateJsPerson() 函数。这里是在对象 o 的作用域中调用的,因此调用后 o 就拥有了所有属性和 writeJs()方法。

构造函数模式的缺点;

p1 和 p2 拥有不同的属性值,但是拥有相同的方法;

var p1 = new CreateJsPerson("zhu", 18);
var p2 = new CreateJsPerson("an", 28);

console.log(p1.writeJs === p2.writeJs);

函数的意义是高内聚,低耦合,避免同样的事情重复创建,所以才把做相同事务的代码封在一起;构造函数中的函数已经失去了函数的意义了;

CreateJsPerson中的writeJs方法,在实例化以后,是不同的;p1.writeJs 并不等于 p2.writeJs;

当然这个问题,也可以通过变种来实现;

function CreateJsPerson(name, age) {
    //浏览器默认创建的对象就是我们的实例p1 -> this
    this.name = name;//->变量p1实例化的时候,那么this就是p1,相当于p1.name=name
    this.age = age;
    this.writeJs = writeJs
    //浏览器在把创建的实例默认的进行返回,无需return
}

function writeJs(){
  console.log("my name is 【" + this.name + "】,i can write js 啦!!");
}

// 当作构造函数使用
var p1 = new CreateJsPerson("zhu", 18);
var p2 = new CreateJsPerson("an", 28);

console.log(p1.writeJs === p2.writeJs);//true

因为writeJs是定义在全局作用域中的,所以p1.writeJs 和 p2.writeJs 的引用地址都是全局中的writeJs,引用地址相等,所以为true;

但是这样做还有一个非常问题,就是CreateJsPerson这个类,依赖writeJs这个方法,两者耦合在一起了,维护起来非常不方便;如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

构造函数的加深理解;
function Fn() {
  this.x = 100;
  this.getX = function () {
    //this->需要看getX执行的时候才知道
    console.log(this.x);
  }
}
var f1 = new Fn;//当f1作为Fn的一个实例的时候,里面的this就是f1
f1.getX();//->方法中的this是f1 ->100
var ss = f1.getX;
ss();//->方法中的this是window ->undefined
  • 1、在构造函数模式中new Fn()执行,如果Fn不需要传递参数的话,后面的小括号可以省略,JS中所有的类都是函数数据类型的,它通过new执行变成了一个类,但是它本身也是一个普通的函数,JS中所有的实例都是对象数据类型的;

  • 2、this的问题:在类中出现的this.xxx=xxx中的this都是当前类的实例,而某一个属性值(方法),方法中的this需要看方法执行的时候,前面是否有"."才能知道this是谁;如果函数只是定义,没有引用,那么里面的this是不知道是谁的,函数只有被引用了,才知道引用被执行时的this指向谁;

  • 3、类有普通函数的一面,当函数执行的时候,var num其实只是当前形成的私有作用域中的私有变量而已,它和我们的f1这个实例没有任何的关系;只有this.xxx=xxx才相当于给f1这个实例增加私有的属相和方法,才和我们的f1有关系...

      function Fn() {
        var num = 10;
        this.x = 100;//f1.x=100
        this.getX = function () {//f1.getX=function...
          console.log(this.x);
        }
      }
      var f1 = new Fn;
      console.log(f1.num);//->undefined
    
  • 4、在构造函数模式中,浏览器会默认的把我们的实例返回(返回的是一个对象数据类型的值);如果我们自己手动写了return返回:返回的是一个基本数据类型的值,当前实例是不变的,例如:return 100; 我们的f1还是当前Fn类的实例;但是返回的如果是一个引用数据类型的值,当前的实例会被自己返回的值给替换掉,例如:return {name:"zhu"} 我们的f1就不在是Fn的实例了,而是对象{name:"zhu"} function Fn() { this.x = 100; this.getX = function () { console.log(this.x); }; return {name: "zhu"}; } var f1 = new Fn; console.log(f1);//此时的f1是{name: "zhu"}

  • 5、检测某一个实例是否属于这个类->instanceof

对于检测数据类型来说,typeof有自己的局限性,不能细分object下的对象、数组、正则...

function Fn() {
    this.x = 100;
    this.getX = function () {
        console.log(this.x);
    };
}
var f1 = new Fn;
console.log(f1 instanceof Array);//->false 说明f1不是一个数组
console.log(f1 instanceof Fn);//->true 说明f1是Fn的一个实例
console.log(f1 instanceof Object);//->true 说明a是一个对象
  • 6、下面f1和f2都是Fn这个类的一个实例,都拥有x和getX两个属性,但是这两个属性是各自的私有的属性,所以

    function Fn() { this.x = 100; this.getX = function () { console.log(this.x); }; } var f1 = new Fn; var f2 = new Fn; console.log(f1.getX === f2.getX);//->false

知识点四:原型模式


在构造函数的理解图里

我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,

function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //"Person prototype"
var person2 = new Person();
person2.sayName(); //"Person prototype"
console.log(person1.sayName == person2.sayName); //true

我们将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName() 函数。

理解原型

  • 节点1. 理解原型对象
  • 节点2. 判断某个属性属于原型,而不是私有属性;
  • 节点3. constructor属性的指向问题
  • 节点4. 原型的动态性
  • 节点5. 原生对象的原型
  • 节点6. 原型模式的问题
节点1. 理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor 指向 Person 。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

总结

  • 1、只要创建了一个新函数Person,系统会自动为该函数创建一个 prototype属性,Person.prototype这个属性指向函数的原型对象。
  • 2、函数的原型对象里自动获得一个constructor(构造函数)属性,Person.prototype.constructor 又指回了 Person;
  • 3、函数的原型对象里自动获得一个__proto__属性,默认指向Object;
  • 4、Person的实例person1自动获得__proto__属性,默认指向Person.prototype;
  • 5、调用person1的方法或者属性,默认会在自己的内部找;如果找不到,会通过person1.__proto__找到Person.prototype;如果Person.prototype再找不到,会通过Person.prototype.proto,一直找到Object这个基类;如果再找不到就会报undefined或者报错;
  • 6、只要创建了一个新函数Person,函数Person本身也会自动获得Person.__proto__的属性,该属性指向函数这个类(anonymous),函数类的__proto__指向Object;

具体的对应逻辑代码如下:

function Person007(){
  var name="Person007"
}
console.log(Person007.prototype);//1、只要创建了一个新函数Person,系统会自动为该函数创建一个 prototype属性,Person.prototype这个属性指向函数的原型对象。
console.log(Person007.prototype.constructor);//2、函数的原型对象里自动获得一个constructor(构造函数)属性,Person.prototype.constructor 又指回了 Person;
console.dir(Person007.prototype.__proto__);//3、函数的原型对象里自动获得一个__proto__属性,默认指向Object;

function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();

console.dir(Person.__proto__);//只要创建了一个新函数Person,函数Person本身也会自动获得Person.__proto__的属性,该属性指向函数这个类(anonymous),函数类的__proto__指向Object;

person1.sayName(); //Person prototype
person1.__proto__.sayName(); //4、Person的实例person1自动获得__proto__属性,默认指向Person.prototype;
person1.__proto__.__proto__.sayName(); //5、调用person1的方法或者属性,默认会在自己的内部找;如果找不到,会通过person1.__proto__找到Person.prototype;如果Person.prototype再找不到,会通过Person.prototype.__proto__,一直找到Object这个基类;如果再找不到就会报undefined或者报错;

深入理解person1的查找原理;

function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();

console.log(person1.name);//来自原型
person1.sayName();//来自原型

person1.name="change name"
console.log(person1.name);//来自实例;

delete person1.name//使用 delete 操作符删除了 person1.name ,之前它保存的 "change name"值屏蔽了同名的原型属性。把它删除以后,就恢复了对原型中 name 属性的连接。因此,接下来再调用person1.name 时,返回的就是原型中 name 属性的值了
console.log(person1.name);//来自原型

** hasOwnProperty :检测一个属性是不是私有属性(这个方法只在给定属性存在于对象实例中时,才会返回 true )**

function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();

console.log(person1.name);//来自原型
person1.sayName();//来自原型
console.log(person1.hasOwnProperty("name"));//false

person1.name="change name"
console.log(person1.name);//来自实例
console.log(person1.hasOwnProperty("name"));//true

delete person1.name
console.log(person1.name);//来自原型
console.log(person1.hasOwnProperty("name"));//false

节点2. 判断某个属性属于原型,而不是私有属性

操作符 in:单独使用和在 for-in 循环中使用。在单独使用时, in 操作符会在通过对象能够访问给定属性时返回 true ,无论该属性存在于实例中还是原型中。

代码如下:

function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();

console.log(person1.name);//来自原型
person1.sayName();//来自原型
console.log(person1.hasOwnProperty("name"));//false
console.log("操作符in","name" in person1);//true

person1.name="change name"
console.log(person1.name);//来自实例
console.log(person1.hasOwnProperty("name"));//true
console.log("操作符in","name" in person1);//true

delete person1.name
console.log(person1.name);//来自原型
console.log(person1.hasOwnProperty("name"));//false
console.log("操作符in","name" in person1);//true

同时使用 hasOwnProperty() 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,

function hasPrototypeProperty(object, name){
	return !object.hasOwnProperty(name) && (name in object);
}

由于 in 操作符只要通过对象能够访问到属性就返回 true , hasOwnProperty() 只在属性存在于实例中时才返回 true ,因此只要 in 操作符返回 true 而 hasOwnProperty() 返回 false ,就可以确定属性是原型中的属性

节点3. constructor属性的指向问题

每添加一个属性和方法就要敲一遍 Person.prototype 。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下面的例子所示

function Person(){
}
Person.prototype = {
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};

将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外: constructor 属性不再指向 Person 了

function Person(){
}
Person.prototype = {
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);

一般写了原型后,就会把函数指回他原来的类;

function Person(){
}
Person.prototype = {
    // constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);

以这种方式重设 constructor 属性会导致它的 [[Enumerable]] 特性被设置为 true。默认情况下,原生的 constructor 属性是不可枚举的

function Person(){
}
Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};

// 重设构造函数,只适用于 ECMAScript 5  兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);
节点4. 原型的动态性
  • 在某个类函数的原型上追加方法和属性;之前创建的的类函数实例,都可以获得新追加的属性和方法;

  • 当重写某个类函数的原型时,之前创建的类函数实例,无法获得重写后的方法和属性;

    function Person(){ } Person.prototype = { constructor : Person, name : "Person prototype", age : 29, job: "WEB", sayName : function () { alert(this.name); } }; var person1=new Person; console.log(person1.addTestKey);//undefined Person.prototype.addTestKey="test value"; console.log(person1.addTestKey);//test value

如果换一种写法,下面这种的,就无法获得重写原型后的属性了;

function Person(){
}
var person1=new Person;
Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};
console.log(person1.addTestKey);//undefined
Person.prototype.addTestKey="test value";
console.log(person1.addTestKey);//undefined

原理是;因为person1创建的时候,里面的person1.proto,指向的是默认的Person.prototype;新改变后的prototype已经改变了;而访问person1.addTestKey的时候,访问的是老地址中的这个属性;

节点5. 原生对象的原型
var testAry=[1,2,3]
console.log(testAry.__proto__==Array.prototype);//true

原生的Array类就是上面的这种思路;testAry这个实例的__proto__,指向数组类的prototype;

也可以通过修改原生对象的原型,来追加方法的;

String.prototype.startsWith = function (text) {
	return this.indexOf(text) == 0;
};
var msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true
console.log(msg.startsWith("world")); //false
节点6. 原型模式的问题
  • 因为原型的动态性,导致实例之间的属性和方法会互相干扰;

所有的属性都是定义在原型上的,所有的实例都会继承原型上的属性和方法;

  function Person(){
  }
  Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    testAry:["test1","test1"]
  };
  var person1=new Person;
  var person2=new Person;

  console.log(person1.testAry,person2.testAry);//思考,为什么此时输出的时候,person1.testAry还会有test-push-person1;而不是["test1","test1"]呢?
  person1.testAry.push("test-push-person1");

改变person1的testAry后,就会影响到person2的testAry的属性;

表现如下

这就是原型模式的弊端

不同的实例之间需要有自己的单独的私有属性和方法,并不能都是老共的!如果每个人的东西都是大家伙的,那就乱套了;上面的构造函数模式中,总结的是构造函数中,每个实例的中的内部属性都是相互独立的,为了解决公用的方法才引入原型模式的概念;这就引申到了公认最佳的创建类的方式构造函数模式+原型模式

思考题:为什么下面person1改变后,并不会影响person的变化;

  function Person(){
  }
  Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
      console.log(this.name);
    }
  };
  var person1=new Person;
  var person2=new Person;

  person1.age+=2;
  person1.sayName=null;

  console.log(person1.age,person1.sayName);
  console.log(person2.age,person2.sayName);

#知识点五:构造函数+原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

  function CreateJsPerson(name, age) {
    this.name = name;
    this.age = age;
    this.like =["one","two"];
  }
  CreateJsPerson.prototype = {
    constructor:CreateJsPerson,
    writeJs:function(){
      console.log("my name is " + this.name + ",i can write js 啦!!");
    }
  };
  var p1 = new CreateJsPerson("zhu1", 18);
  var p2 = new CreateJsPerson("zhu2", 28);
  p1.writeJs();//->my name is zhu1,i can write js 啦!!
  p2.writeJs();//->my name is zhu2,i can write js 啦!!
  p1.like.push("p1-like");
  console.log(p1.like);//["one", "two", "p1-like"]
  console.log(p2.like);//["one", "two"]
  console.log(p1.writeJs === p2.writeJs);//->true
  console.log(p1.like === p2.like);//->false

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 writeJs() 则是在原型中定义的。而修改了 p1.like (向其中添加一个新字符串),并不会影响到 p2.like ,因为它们分别引用了不同的数组。

这种构造函数与原型混成的模式,是目前在 ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

1、构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立开的->实例识别

基于构造函数模式的原型模式解决了方法或者属性公有的问题->把实例之间相同的属性和方法提取成公有的属性和方法 ->想让谁公有就把它放在CreateJsPerson.prototype上即可

  • 1、每一个函数数据类型(普通函数、类)都有一个天生自带的属性:prototype(原型),并且这个属性是一个对象数据类型的值

  • 2、并且在prototype上浏览器天生给它加了一个属性constructor(构造函数),属性值是当前函数(类)本身

  • 3、每一个对象数据类型(普通的对象、实例、prototype...)也天生自带一个属性:proto,属性值是当前实例所属类的原型(prototype)

    function Fn() {
      this.x = 100;
      this.sum = function () {
      };
    }
    Fn.prototype.getX = function () {
      console.log(this.x);
    };
    Fn.prototype.sum = function () {
    };
    var f1 = new Fn;
    var f2 = new Fn;
    console.log(Fn.prototype.constructor === Fn);//->true
    
2、Object是JS中所有对象数据类型的基类(最顶层的类)

  //1)、f1 instanceof Object ->true 因为f1通过__proto__可以向上级查找,不管有多少级,最后总能找到Object

  //2)、在Object.prototype上没有__proto__这个属性

  function Fn() {
    this.x = 100;
    this.sum = function () {
    };
  }
  Fn.prototype.getX = function () {
    console.log(this.x);
  };
  Fn.prototype.sum = function () {
  };
  var f1 = new Fn;
  var f2 = new Fn;
//  console.log(Fn.prototype.constructor === Fn);//->true
  console.log(f1 instanceof  Object);//->true
  console.dir(f1);//->原型,原型链的查找;

##### 3、原型链模式

知识点六:其它扩展模式

一般包括下面三种

- 知识点六.1:动态原型模式(利用原型的动态性)
- 知识点六.2:寄生构造函数模式(构造函数模式+工厂模式)
1.动态原型模式(利用原型的动态性)

应用场景:这是一个在原型模式上的优化方法; 原理:对原型所做的修改,能够立即在所有实例中得到反映,利用了原型的动态性;

运行思路:可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。只有某个方法不存在的情况下,才会将它添加到原型中。if中的代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。有点类似图片延迟加载的思路;分批实现,从而提高脚本的性能;

  function Person(name, age, job){
    //属性
    this.name = name;
    this.age = age;
    this.job = job;
    
    // 方法
    if (typeof this.sayName != "function"){
      Person.prototype.sayName = function(){
        console.log(this.name);
      };
    }
  }
  var friend = new Person("Nicholas", 29, "Software Engineer");
  friend.sayName();

对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定它的类型。仅仅只是一个判断语句,用什么方法来判断,就根据所在的场景来实现的了;

2.寄生构造函数模式(构造函数模式+工厂模式)

通常,在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。

表现形式:工厂模式的代码;却用构造函数来实例化类;

  function SpecialArray(){
    this.name="SpecialArray";
    this.age="2222";

    var values = new Array();
    values.push.apply(values, arguments);
    values.toPipedString = function(){
      return this.join("|");
    };
    return values;
  }
  var colors = new SpecialArray("red", "blue", "green");
  console.log(colors);
  console.log(colors.toPipedString()); //"red|blue|green"

构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

    this.name="SpecialArray";
    this.age="2222";

这两行代码在加上return这个语句后,相当于白写了;

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式。

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。