JavaScript面试考点之原型及原型链
1、JavaScript原型及原型链
1)原型
JavaScript是一种基于原型的语言,即每一个对象拥有一个原型对象。当我们试图去访问一个对象的属性时,不仅在该对象去搜寻,还会去对象的原型,以及该对象原型的原型,依次层层想上搜索,直到找到或到达原型链末尾。
这些属性和方法定义在Object的构造器函数的prototype属性上,而非实例本身。
函数可以有属性。每个函数都有一个特殊的属性叫作原型prototype。
a、每一个函数数据类型(普通函数、类)都有一个自带属性:prototype(原型),并且这个属性是一个对象数据类型的值。
b、并且在prototype上浏览器给它加上了一个属性constructor(构造函数),属性值时当前函数(类)本身;
c、每一个对象数据类型(普通对象、实例、prototype)也自带一个属性__proto__,属性值是当前实例所属类的原型(prototype)。
原型对象有一个自有属性constructor,这个属性指向该函数。关系图如下:
2)原型链
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
构造函数Person存在原型对象是Person.prototype;
构造函数生成实例对象person,person的__proto__指向构造函数Person原型对象;
Person.prototype.__proto__ 指向内置对象,因为 Person.prototype 是个对象,默认是由 Object函数作为类创建的,而 Object.prototype 为内置对象;
Person.__proto__指向内置匿名函数anonymous,因为 Person 是个函数对象,默认由 Function 作为类创建。
Function.prototype和Function.__proto__同时指向内置匿名函数anonymous,这样原型链的终点就是null。
总结:
__proto__和prototype关系:__proto__和constructor是对象独有的。prototype属性是函数独有的
__proto__作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的。
每个对象的__proto__都是指向它的构造函数的原型对象prototype的;
构造函数是一个函数对象,是通过 Function构造器产生的;
原型对象本身是一个普通对象,而普通对象的构造函数都是Object;
所有的构造器都是函数对象,函数对象都是 Function构造产生的;
Object的原型对象也有__proto__属性指向null,null是原型链的顶端。
a、一切对象都是继承自Object对象,Object 对象直接继承根源对象null
b、一切的函数对象(包括 Object 对象),都是继承自 Function 对象
c、Object 对象直接继承自 Function 对象
d、Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
js 获取原型的方法:
p.proto
p.constructor.prototype
Object.getPrototypeOf(p)
思考:Number.prototype.constructor === Number.constructor 结果相等吗?
答案是不相等。因为Number.prototype.constructor 查找的是自己 Number 原型上的构造函数。Number是没有有 constructor 属性,就会原型链的 __proto__ 向上查找上一级类(这里是函数)的原型,函数的 constructor 指向 Function。所以不同。
2、判断属性是否继承自原型链
JavaScript中Object对象原型上的hasOwnProperty()用来判断一个属性是定义在对象本身而不是继承自原型链。
因为JavaScript没有将hasOwnProperty作为一个敏感词,所以我们很有可能将对象的一个属性命名为hasOwnProperty,这样一来就无法再使用对象原型的 hasOwnProperty 方法来判断属性是否是来自原型链。
则我们需要使用原型链上真正的 hasOwnProperty 方法
3、属性遍历方法
Object.keys() 方法会返回一个由给定对象的所有可枚举自身属性的属性名组成的数组,不包括继承自原型的属性和不可枚举的属性。
Reflect.ownKeys()返回所有自有属性key,不管是否可枚举,但不包括继承自原型的属性。
for in主要用于遍历对象的可枚举属性,包括自有属性、继承自原型的属性。
Object.assign() //会忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
使用Object.defineProperty()方法设置enumerable,Object.defineProperty(obj, prop, descriptor)方法有三个参数
obj:目标对象
prop:目标属性,字符串
descriptor:对目标属性的行为,放在对象里
enumerable为true时表示可枚举,enumerable为false表示不可枚举;
每个对象都有propertyIsEnumerable()方法,这个方法可以判断出指定的属性是否可枚举。propertyIsEnumerable方法只对对象自身的属性(对象自身添加的、构造函数实例化的)有效,对原型上的、继承来的属性都无效。
obj.propertyIsEnumerable("属性名");
1)这个属性必须属于实例的,并且不属于原型。
2)这个属性必须是可枚举的。
3)如果对象没有指定的属性,该方法返回false
4、对new的理解
new操作符用于创建一个给定构造函数的实例对象。
new的流程:
a、创建一个新的对象obj;
b、将对象与构建函数通过原型链连接起来;
c、将构建函数中的this绑定到新建的对象obj上;
d、根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理。(new 关键词执行之后总是会返回一个对象,要么是实例对象,要么是 return 语句指定的对象)
当构造函数最后return出来的是一个和this无关的对象时,new 命令会直接返回这个新对象,而不是通过 new 执行步骤生成的 this 对象。
但是这里要求构造函数必须是返回一个对象,如果返回的不是对象,那么还是会按照 new 的实现步骤,返回新生成的对象。
手写new
a、使用Object.create将obj 的proto指向为构造函数的原型;
b、使用apply方法,将构造函数内的this指向为obj;
c、在create返回时,使用三目运算符决定返回结果。
构造函数如果有显式返回值,且返回值为对象类型,那么构造函数返回结果不再是目标实例
5、继承
1)原型继承
直接让子类的原型对象指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承。
缺点:它是通过链式继承的,属于引用类型传值,不是复制一份,引用副本实例属性的修改必然会引起其他副本实例属性的修改。
2)call继承(构造函数继承)
构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,让父类的构造函数把成员属性和方法都挂到子类的this上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参。
复制一份,子类的实例各自得到一份构造函数的副本,属于值传递,所以子类之间的属性修改是互不相关的,跟父类就没有关系了。
缺点:继承不到父类原型上的属性和方法。
3)组合式继承
原型继承+call继承。在子类的构造函数中通过Parent.call(this)继承父类的属性,然后改变子类的原型为new Parent()来继承父类的函数。
缺点:每次创建子类实例都执行了两次构造函数(Parent.call()和new Parent()),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法
4)寄生组合式继承
为了解决每次创建子类实例都执行了两次构造函数的问题,私有的只拿私有的,用call来做。共有只拿共有的,用Object.create()来做。