JS核心知识点梳理——原型

305 阅读8分钟

引言

有些人认为 JavaScript 不是真正的面向对象的语言,比如它没有像许多面向对象的语言一样有用于创建class类的声明(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)

JavaScript 用一种称为构造函数的特殊函数来定义对象和它们的特征。

不像“经典”的面向对象的语言,从构造函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。同理,原型链也是实现继承的主要方式(ES6的extends只是语法糖)。

原型

定义

原型就是构造函数上的prototype属性所指向的一个对象,

这个对象叫原型对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

属性间关系

实例的__proto__指向原型

原型的constructor指向构造函数

构造函数的prototype属性指向原型

补充: __proto__已被弃用,提倡使Object.getPrototypeOf(obj)

图例

function Person() { }
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function () { 
    alert(this.name);
};
var person1 = new Person();
var person2 = new Person();

原型.png

原型链

定义

原型对象1是一个对象,那么它也有构造函数,换句话说原型也有__proto__属性指向它构造函数的原型2.

原型对象2是一个对象,那么它也有构造函数,换句话说原型也有__proto__属性指向它构造函数的原型3.

....那么上述关系依然成立,如此层层递进,就构成了实 例与原型的链条。这就是所谓原型链的基本概念

实例对象访问某个属性、方法的时候,如果实例对象本身找不到,则通过自己的原型链不断往上层类的原型去访问,直到找到,找不到则报错

图例:

var arr = [1,2,3] //arr是一个实例对象(数组类Array的实例)
arr.__proto__ === Array.prototype //true  实例上都有一个__proto__属性,指向“类”的原型
Array.prototype.__proto__ === Object.prototype //true “类”的原型也是一个Object实例,那么就一定有一个__proto__属性,指向“类”object的原型

浏览器在在Array.prototype上内置了pop方法

在Object.prototype上内置了toString方法

image.png 上图是我画的一个原型链图

[1,2,3].pop() //3
[1,2,3].toString() //'1,2,3'
[1,2,3].constructor.name //"Array" 
[1,2,3].hehe() //[1,2,3].hehe is not a function

当我们调用pop()的时候,在实例[1,2,3]上面没有找到该方法,则沿着原型链搜索"类"Array的原型,找到了pop方法并执行,同理调用toString方法的时候,在"类"Array没有找到则会继续沿原型链向上搜索"类"Object的原型,找到toString并执行。 当执行hehe方法的时候,由于“类”Object的原型上并没有找到,搜索“类”Object的原型的__proto__,由于执行null,停止搜索,报错。

注意,[1,2,3].constructor.name显示‘Array’不是说明实例上有constructor属性,而是正是因为实例上没有,所以搜索到的原型上了,找到了constructor

类,创建对象的方法

怎么创建对象,或者说怎么模拟类。这里我就不学高程一样,给大家介绍7种方法了,只讲我觉得必须掌握的。毕竟都es6 es7了,很多方法基本都用不到,有兴趣自己看高程。

利用构造函数

 const Person = function (name) {
        this.name = name
        this.sayHi = function () {
            alert(this.name)
        }
    }
    const xm = new Person('小明')
    const zs = new Person('张三')
    zs.sayHi() //'张三'
    xm.sayHi() //'小明'

缺点: 每次实例化都需要复制一遍函数到实例里面(堆内存里开辟空间存储)。但是不管是哪个实例,实际上sayHi都是相同的方法,没必要每次实例化的时候都复制一遍,增加额外开销。

组合使用原型和构造函数

    //共有方法挂到原型上
    const Person = function () {
         this.name = name
    }
    Person.prototype.sayHi = function () {
            alert(this.name)
        }
    const xm = new Person('小明')
    const zs = new Person('张三')
    zs.sayHi() //'张三'
    xm.sayHi() //'小明'

缺点:基本没啥缺点了,创建自定义类最常见的方法,动态原型模式也只是在这种混合模式下加了层封装,写到了一个函数里面,好看一点,对提高性能并没有卵用。

es6的类

es6的‘类’class其实就是语法糖

class Person {
   constructor(name) {
   	this.name = name
  }
  say() {
   	alert(this.name)
  }
}
const xm = new Person('小明')
const zs = new Person('张三')
zs.sayHi() //'张三'
xm.sayHi() //'小明'

在es2015-loose模式下用bable看一下编译

"use strict";

var Person =
/*#__PURE__*/
function () {
  function Person(name) {
    this.name = name;
  }

  var _proto = Person.prototype;

  _proto.say = function say() {
    alert(this.name);
  };

  return Person;
}();

分析:严格模式,高级单例模式封装了一个类,实质就是组合使用原型和构造函数

寄生构造函数模式

比如现在需要创建一些特殊的数组,这些数组有sayContent方法,可以打印出自己的内容。

第一个想法是给Array.prototype额外添加一个sayContent方法 但是这样的话 所有的Array实例都会有这个方法,这个是我们不愿意看到的

第二个思路是直接在创建出来的实例上添加方法不去动原型

function specialArray() {
		var arr = new Array(...arguments)
		arr.sayContent = function () {
			 console.log([...this.values()])
		}
		return arr
	}
	var arr = new specialArray('x','y','z')

其实还有第三个想法 需要用到原型继续的思路,扩展性更强,vue对数组编译方法的处理就是这个思路, 建立一个起隔离作用的原型。横在数组实例和Array之间 我们在这个原型上添加方法和属性

实例[[proto]]--->隔离原型

隔离原型[[proto]]---->Array.prototype

这个下一章讲

JS世界里的关系图

image.png 知识点:

1. Function.__proto__ === Function.prototype   //`方法` Function是`构造函数`Function的实例,没毛病
2. Object.__proto__ === Function.prototype //`方法`Object是`构造函数`Function的实例,没毛病
3. 任何方法上都有prototype属性以及__proto__属性
   任何对象上都有__proto__属性
4. Function.prototype.__proto__===Object.prototype
5. Object.prototype.__proto__ = null//`这也就是沿着原因链不会一直搜索的原因,搜到null就停止了,也就是Object的原型那一层没有就停止了`  

注意这个Function 它既有prototype了属性 这是把它当构造函数了
又有__proto__这是把它当实例了
同理Object

判断类型的方法

之前在JS核心知识点梳理——变量里面说过了,这里借着原型再来回顾下

1. typeof:

只能判断基础类型中的非Null,不能判断引用数据类型(因为全部为object)它是操作符

2. instanceof:

用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置 风险的话有两个

//判断不唯一
[1,2,3] instanceof Array //true
[1,2,3] instanceof Object //true
//原型链可以被改写
const a = [1,2,3]
a.__proto__ = null
a instanceof Array //false

仿写一个instanceof,并且挂在Object.prototype上,让所有对象都能用

//仿写一个instance方法
	Object.prototype.instanceof = function (obj) {
		let curproto = this.__proto__
		while (!Object.is(curproto , null)){
			if(curproto === obj.prototype){
				return true
			}
			curproto = curproto.__proto__
		}
		return false
	}
   
[1,2,3].instanceof(Array) //true
[1,2,3].instanceof(Object) //true
[1,2,3].instanceof(Number) //false
[1,2,3].instanceof(Function) //false
1..instanceof(Function) //false
(1).instanceof(Number) //true

3. constructor:

constructor 这玩意已经介绍过了,“类”的原型执行constructor指向“类” 风险的话也是来自原型的改写

[1,2,3].constructor.name //'Array'

// 注意下面两种写法区别
Person.protorype.xxx = function //为原型添加方法,默认constructor还是在原型里
Person.protorype = { //原型都被覆盖了,没有constructor了,所要要手动添加,要不然constructor判断失效
   xxx:function
   constructor:Person
}

4.Object.prototype.toString.call(xxx)

最准的一个方法,完全可以根据这个方法封装一个方法

Object.prototype.toString.call([1,2,3])   //"[object Array]"
Object.prototype.toString.call(function(){}) //"[object Function]"
Object.prototype.toString.call(1) //"[object Number]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call({}) //"[object Object]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call('string') //   "[object String]" 

5.其他判断方法比较

  • isNaN 可以判断是否是NaN,但是注意会先转化为Number,所以不一定准,可以先判断是不是number数据类型再用 ` isNaN(NaN) //true isNaN('1px') //true

function istrueNaN(val){ return typeof val ==='number' && isNaN(val) } `

  • isArray 判断是不是数组,比Object.prototype.toString.call(xxx)简单 然后写到这我又想到一个问题,如果是一个类数组呢,比如arguments 类数组的定义,怎么判断,以及类数组怎么调用数组的方法?

  • Object.is(a,b) 这个方法主要判断a,b是否相等 对于我来说可以大幅简化NaN和null的判断

Object.is(null, null);       // true
Object.is(NaN, NaN);       // true

in操作符

对于for in 和in 都是沿着原型链查找属性是否存在,可以利用hasOwnProperty进行相关过滤

// 'in' operation test
class Person {
		constructor (name) {
			this.name = name
		}
		sayHi() {
			console.log('Hi')
		}
	}
	var p1 = new Person('小明')


'name' in p1 //true
'sayHi' in p1 //true 

for (var i in p1) {
		if (p1.hasOwnProperty(i)) {
			console.log('ownProperty:' + i)
		} else {
			console.log('prototypeProperty: ' + i)
		}
	}
//'ownProperty: name'
// prototypeProperty: sayHi

in 配合hasOwnProperty略显麻烦,就没有自不到位的遍历自身属性的方法吗?有的

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。

class Person {
		constructor (name) {
			this.name = name
		}
		sayHi() {
			console.log('Hi')
		}
	}
	var p1 = new Person('小明')
    Object.entries(p1)
    // [['name','小明']]
类似的还有 Object.keys()   Object.values()  
    Object.keys(p1)
    //['name']
    Object.values(p1)
    //['小明']