1

前言

粗略记录一下,欢迎各位大佬纠正(ORZ)

更新记录

2019.9.5 修改了对象有constructor 属性的错误,正确的是对象的constructor 属性是来自构造函数的原型对象的(fn.prototype.constructor )

内容

  • JS的原型链
  • JS继承
  • JS设计模式

JS的原型链

一、首先,JS里几乎所有值都是对象(使用的时候)!

我们知道,JS基础数据类型是 number,string,boolean,undefined和null,而引用类型就object,
之前看的时候,我很奇怪为什么像var str = "";这个str明明的类型是String,为什么它却可以引用String.prototype原型对象的属性和方法呢,并且它确实有对象才有的__proto__

var str = '';      

console.log(str.__proto__ === String.prototype) //true

console.log(str.constructor === String) //true

console.log(str instanceof String); //false,前面两个都符合了,这个竟然是返回false,不是String的实例

console.log(String.prototype.isPrototypeOf(str)); //false,跟instanceof功能是一样的

后面百度查了一下,原因是

读取字符串的时候会创建一个对象,但是这个对象只是临时的,所以我们称它为临时对象,学术名字叫包装对象,说它临时,是因为我们在读取它的属性的时候,js会把这个string字符串通过new String()方式创建一个字符串对象,一旦引用结束,这个对象就被销毁了。

所以说就是像读取对象那样读取属性的时候,暗地里帮我new String()了,不读取的时候,就是基础类型,所以判断是不是实例才返回了false

str.name = 'nihao';  //可以这样写不报错,因为暗地里帮我对象化了,
str.name         //可以点name出来,但是是undefined,没错

总结:目前发现除了undefined和null不能这样搞,其他类型都是有__proto__,所以说,JS几乎所有值都为对象

二、__proto__,prototype,和constructor关系

首先,要明确两点的是

  • 函数才有prototype属性,这个属性指向的那个对象我们一般也叫做原型对象
  • constructor属性是在原型对象上面的!也就是fn.prototype.constructor,这个指向的是构造函数fn,在对象里面之所以能够使用,是因为这个属性拿的就是原型对象上面
  • 对象才有__proto__属性,指向构造函数的原型对象,也就是函数.prototype,那函数有没有?肯定有啊,函数本来就是属于引用数据类型的一种,就是Object

好了,那明确这两点之后,再说说这三者有什么关系,先放图吧

clipboard.png
先说一下这个,按照这个图的意思,有一个构造Person函数,这个函数默认就会有prototype属性,这个属性指向的值是一个对象,我们叫做原型对象,然后呢,Person.prototype这个原型对象,它也会有一个constructor属性,这个属性默认指回构造函数,也就是Person函数,原型对象的name,age这些就是我们自己往这个对象加的,Person.prototype.name = 'xxx',就像这样,然后
两个实例,person1,person2这两个对象,有__proto__属性对吧,它指向是构造函数的原型对象,就是Person.prototype

//构造函数Person
function Person(){}
//往原型对象加值
Person.prototype.name = 'mychirs';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
  alert(this.name);
};

//两个实例
var person1 = new Person();
var person2 = new Person();

console.log(Person.prototype.constructor === Person)  //true
console.log(person1.constructor === Person.prototype.constructor)  //true
console.log(person1.__proto__ === Person.prototype)  //true
console.log(person2.__proto__ === Person.prototype)  //true

好了,说完上面那个图已经差不多了,再放多一张图

clipboard.png

这个图其实补充了两点,
第一,原型链的尽头是Object.prototype.__proto__,值为null
第二,Function.constructor这个值,正常来说,应该是指向实例Function这个函数的更上一个构造函数的原型对象的constructor,但是这张图已经没了,因为Function这个已经是最高的构造函数了,Function.constructor还是Function.prototype.constructor

//构造函数Person
function Person(){}

var obj = {}
var person1 = new Person()


console.log(Person.constructor.constructor.constructor  === Function)  //true,一直点下去都是这样

console.log(obj.constructor === Object.prototype.constructor )  //true

console.log(obj.constructor.prototype.__proto__=== null)  //true,原型链尽头,null

JS的继承

JavaScript 语言的继承不通过 class(ES6 引入了class 语法),而是通过“原型对象”(prototype)实现,一般来说,

如果属性和方法在实例里找不到的话,会通过实例,也就是对象的__proto__,属性,找到构造函数的原型对象(fn.prototype),在这里面找属性和方法,如果再找不到的的话,原型对象也是对象是吧,所以它就会通过fn.prototype.__proto__,找到对象构造函数的原型对象,也就是(Object.prototype)这里找,如果再找不到,就到尽头拉,因为Object.prototype.__proto__会返回null了。
下面介绍几种常见继承方式。

一、原型链继承

       //父类型
       function Person(name, age) {
           this.name = name,
           this.age = age,
           this.play = [1, 2, 3]
           this.setName = function () { }
       }
       Person.prototype.setAge = function () { }
       
       //子类型
       function Student(price) {
           this.price = price
           this.setScore = function () { }
       }
       Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
       var s1 = new Student(15000)
       var s2 = new Student(14000)
       console.log(s1,s2)

分析之前,先大概说这个new 关键字在实例对象的时候做了什么操作,其实就三步。

var obj  = {};
obj.__proto__ = F.prototype;
F.call(obj);
  • 第一行,我们创建了一个空对象obj;
  • 第二行,我们将这个空对象的__proto__成员指向了F函数对象prototype成员对象;
  • 第三行,我们将F函数对象的this指针替换成obj,然后再调用F函数.

基于上面介绍,那我们现在就重点看看 Student.prototype = new Person() 这句代码就行了,可以分为两步:

  1. new Person()创建了一个空对象,__proto__属性值向了Person.prototype,里面的变量值全部为undefined,(没有传参数嘛)所以应该是这样
{
     name:undefined,
     age:undefined,
     play:[1, 2, 3],
     setName:function () { },
     __proto__:Person.prototype
}

2.然后这个值赋给了Student.prototype, 后面,当我们访问Student的实例的时候,它先会在自身属性找对应属性和方法,找不到就会去Student.prototype这里找,因为我们Student.prototype已经赋值为new Person,所以当找不到的话,会再沿着Student.prototype.__proto__指向的Person.prototype上面找

这里有一个知识点要补充一下,注意!原型对象上面的引用数据类型会共享,基础数据类型不会

var s1 = new Student()
var s2 = new Student()
s1.play.push(4)
console.log(s1.play)  //[1, 2, 3, 4]
console.log(s2.play)  //[1, 2, 3, 4]

总结:
要点:子类的原型赋值为父类的一个实例对象。
缺点:
1.父类的属性和方法都往子类的原型对象上面加,如果这时候父类属性有引用数据类型的话将会共享
2.创建子类实例时,无法向父类构造函数传参

二、借用构造函数继承

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.play = [1,2,3]
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}

  function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age)
    this.price = price
  }

这种虽然可以传参了,引用类型也不会相互影响了,但是我们也很明显的发现,它其实只把Person里面的this.name那些全部搬到了Student里面而已,这里还会有一个问题,就是函数没有复用,每一个对象里面都会再写一次函数,尽管代码是一模一样的。最后就是没有动过原型链,所以Person.prototype的属性和方法是一个也拿不到的

var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Tom', 20, 15000)

s1.play.push(4)

s1.play   //[1,2,3,4]

s2.play   //[1,2,3]

s1.setAge  //undefined

总结:
要点:在子类构造函数中通用call()调用父类构造函数。
缺点:
1.只能继承父类的实例属性和方法,不能继承原型属性和方法

三、原型链+借用构造函数的组合继承

        function Person(name, age) {
            this.name = name
            this.age = age
            this.play = [1,2,3]
            this.setAge = function () {console.log('我是person类实例函数') }
        }
        Person.prototype.plays = [9,9,9]
        Person.prototype.setAges = function () {
            console.log("我是person原型对象的函数")
        }
        
        function Student(name, age, price) {
            Person.call(this,name,age)
            this.price = price
            this.setScore = function () { }
        }
        Student.prototype = new Person()
        //Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
        Student.prototype.sayHello = function () { }

从代码可以看出,在子类用了call,然后也把子类的原型对象赋值为父类的实例,把上面两个结合在一起用而已,我们再看看上面代码,我故意在Person的构造函数加了一个setAge的方法,在Person原型对象加了一个数组,其实我想说的是,这种方法是可以解决上面那两个方式的问题,但是这个前提是在你遵循一定的规范,比如不要在构造函数加方法,不要在原型对象加引用类型的数据,不然一样还是有问题

var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Tom', 20, 15000)

//在构造函数里面的不会有影响
s1.play.push(4)
s1.play   //[1,2,3,4]
s2.play   //[1,2,3]

//原型对象上面的会共享
s1.plays  //[9,9,9]
s1.plays.push(9)
s2.plays  //[9, 9, 9, 9]

s1.setAge ==  s2.setAge  //false,在构造函数里方法没有复用
s1.setAges ==  s2.setAges  //true,原型对象上面的方法就是复用

总结:
要点:在子类构造函数中通用call()调用父类型构造函数。然后又把子类的原型对象赋值为父类的实例
缺点:
1.调用了两次构造函数

四、寄生组合式继承

上面我们说到,构造加原型链继承的组合继承会执行两次new操作,下面这个方式就是为了解决这个调用两次的缺点所诞生的,也算目前最合适的方案。

        function Person(name, age) {
            this.name = name
            this.age = age
            this.play = [1,2,3]
            this.setAge = function () {console.log('我是person类实例函数') }
        }
        Person.prototype.names= '父类原型名字'
        Person.prototype.plays = [9,9,9]
        Person.prototype.setAges = function () {
            console.log("我是person原型对象的函数")
        }
        
        function Student(name, age, price) {
            Person.call(this,name,age)
            this.price = price
            this.setScore = function () { }
        }
        Student.prototype = Object.create(Person.prototype) //就是这里不一样
        //Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
        Student.prototype.sayHello = function () { }

我们看看代码,这个方法唯一的不同就是把Student.prototype = new Person()换成了Student.prototype = Object.create(Person.prototype)我先大概说一下Object.create(),

object.create() 接收两个参数:

  • 一个用作新对象原型的对象
  • (可选的)一个为新对象定义额外属性的对象
//这个对象用来做原型对象
var person = {
    name: '我是原型name',
    plays:[1,2,3]
}

var s1 = Object.create(person)
s1   //{}空对象
s1.__proto__ === person //true,原型对象此时就是person
s1.name     //'我是原型name' 自身属性没有,拿原型对象里面的

var s2 = Object.create(person,{
    name:{
        value: '我自己的name'
    }
})
s2  //{name: "我自己的name"}
s2.__proto__ === person //true,原型对象此时就是person
s2.name     //'我自己的name' 自身属性就有

s1.plays.push(4)
s1.plays   //[1, 2, 3, 4],原型对象的引用数据类型是会共享的
s2.plays   //[1, 2, 3, 4],原型对象的引用数据类型是会共享的

介绍完这个之后,我们就可以回头看看这个语句Student.prototype = Object.create(Person.prototype),
这句话,把我们的Student的原型对象的.__proto__ 指向了Person的原型对象,这样,当我们访问Student的实例,比如s1.xxx,它会访问自身,如果没有,这时候s1.__proto__ 指向Student.prototype,如果Student.prototype又没有,这时Student.prototype.__proto__ 指向Person.prototype,所以就会去到Person.prototype上面找。

我们知道Student.prototype = new Person(),这句话其实跑了两个作用,第一个作用跟Object.create一样,调整了__proto__ 的指向,第二个作用,其实它也把Person构造函数的this.name这些也往Student.prototype这上面加了,只是我们在访问实例属性的时候,由于实例里面已经有,(用了call嘛)所以才不会读到原型对象上面的,所以这也是这个方案的优势。

总结:
要点:用Object.create(),控制子类的原型对象的__proto__ 指向父类的原型对象
缺点:
1.暂无

JS的设计模式

我只写几个常见的,因为我百度了一下好像有好多种 = =

一、单例模式

这个模式就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。(咱们平时用的window就是一个单例)

function Person(name,age){
    this.name = name;
    this.age= age;
}

var getPerson = (function(){
    let ins = null;
    return function(name,age){
        if(!ins){
            ins = new Person(name,age)
        }
        return ins;
    }
})()

注意看看getPerson 这个函数就行了,由于函数里面又返回了一个函数,所以形成了一个闭包,ins 这个变量不会被销毁。

var p1 = new getPerson('你好',100)
var p2 = new getPerson('好你',99)
p1  //{name: "你好", age: 100}
p2  //{name: "你好", age: 100}
p1 === p2   //true

因为p1实例化的时候,ins变量已经有值,所以当p2也实例化的时候,getPerson只会直接返回p1那个实例,不会进行第二次new操作,所以这两个是相等的

一、工厂模式

工厂模式是指提供一个创建对象的接口而不保留具体的创建逻辑,可以根据输入类型创建对象。让子类自行决定实例化哪一种工厂类,实际的创建对象过程在子类中进行。我们下面上代码解释一下

let UserFactory = function (role) {
  function SuperAdmin() {
    this.name = "超级管理员",
    this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理']
  }
  function Admin() {
    this.name = "管理员",
    this.viewPage = ['首页', '通讯录', '发现页', '应用数据']
  }
  function NormalUser() {
    this.name = '普通用户',
    this.viewPage = ['首页', '通讯录', '发现页']
  }

  switch (role) {
    case 'superAdmin':
      return new SuperAdmin();
      break;
    case 'admin':
      return new Admin();
      break;
    case 'user':
      return new NormalUser();
      break;
    default:
      throw new Error('参数错误, 可选参数:superAdmin、admin、user');
  }
}

//调用
let superAdmin = UserFactory('superAdmin');
let admin = UserFactory('admin') 
let normalUser = UserFactory('user')

UserFactory就是一个简单工厂,在该函数中有3个构造函数分别对应不同的权限的用户。当我们调用工厂函数时,只需要传递superAdmin, admin, user这三个可选参数中的一个获取对应的实例对象

暂告一段落

参考链接:
挺好的原型对象说明文章
原型链说明文章
JavaScript常见的六种继承方式
JS原型链与继承别再被问倒了
JavaScript 单例模式
从ES6重新认识JavaScript设计模式(二): 工厂模式


bug之所措
406 声望13 粉丝