前言
粗略记录一下,欢迎各位大佬纠正(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
好了,那明确这两点之后,再说说这三者有什么关系,先放图吧
先说一下这个,按照这个图的意思,有一个构造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
好了,说完上面那个图已经差不多了,再放多一张图
这个图其实补充了两点,
第一,原型链的尽头是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() 这句代码就行了,可以分为两步:
- 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设计模式(二): 工厂模式
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。