JavaScript 学习笔记 之 原型 (一)

[[Prototype]]简介

关于[[prototype]](__proto__)和prototype对象的不同以及原型链是什么

  1. [[prototype]]几乎所有对象都有的一个属性,存放的是一个对其他对象的引用,在部分浏览器(在ES6之前不是所有)中可以用.__proto__(两根下划线)这个属性(事实上这个属性也并不直接存在于这个对象中)来进行访问,每个普通的对象的[[prototype]]链最后都会指向Object.prototype这个对象
  2. prototype是所有的函数会拥有的一个属性,也是指向一个对象,而使用new操作符使用这个函数创建的每一个对象最终也会被[[prototype]]连接到这个.prototype对象,而这个.prototype对象有一个.constructor属性,默认指向这个函数
  3. 当对象上没有找到想要的属性或者方法的时候,引擎就会在[[prototype]]关联的对象上继续找,如果在这个对象上又没找到,那么会继续找这个对象的[[prototype]],这就是所谓的原型链

然后我们来详细介绍下[[prototype]]这个属性

我们可以通过Object.create(..)来关联对象的[[Prototype]]属性

你可能会对这个属性感到陌生,但事实上你可能经常间接地调用过这个属性

比如在访问对象的属性时会触发一个[[Get]]操作(比如 obj.a这个操作)

对于默认的[[Get]]操作来说,如果对象没有这个属性,会访问这个对象完整的的[[Prototype]]链

直到找到这个属性或者查找完整条[[Prototype]]链然后返回这个属性或者一个undefined值

		var obj1 = {
			a: 1
		}
		var obj2 = Object.create(obj1); //将obj2的[[Prototype]]关联到obj1
		console.log(obj2.hasOwnProperty("a")); //false,对象本身没有这个属性
		console.log(obj2.a); //1

使用for .. in遍历对象时,原理和查找原型链类似,任何可以通过原型链访问到的可枚举的属性都会被枚举

使用in操作符的时候,同样会查找对象的整条原型链(无论是否可枚举)

		var obj1 = {
			a: 1
		}
		var obj2 = Object.create(obj1); //将obj2的[[Prototype]]关联到obj1
		for(let i in obj2) {
			console.log(i); //a
		}
		console.log("a" in obj2); //true

因此,你通过各种语法进行属性查找的时候都会遍历整个原型链,一直到内置的Object.prototype

因为普通对象的原型链顶层都是这个Object.prototype对象

扫描二维码关注公众号,回复: 3332208 查看本文章

所以这个对象也包含了JavaScript中许多通用的功能

比如.toString()或者.valueOf()

属性设置和屏蔽

说完对象属性的[[Get]]操作,我们来说说对应的[[Put]]操作

[[Put]]操作也远不是简单的添加一个新属性或者修改一个属性已有的值那么简单

比如 obj.a= "123" 这个过程

  1. 如果a这个属性直接存在于obj中,那么很简单,只会修改obj中b这个属性的值(如果原型链上也有这个属性,那么会发生屏蔽obj中的属性会屏蔽掉原型链中的属性,因为obj.a总是会选择原型链最底层的a属性)
  2. 如果a不直接存在于obj中,那么原型链就会被遍历,如果遍历完整个原型链都找不到,则在obj中添加一个a属性并赋值
  3. 如果a不直接存在于obj中,但是原型链中又有这么个属性,那么又需要另外分3种情况

当a不存在于obj中但是存在于原型链中时, obj.a= "123" 会发生的几种情况

  1. 如果原型链上层存在名为a的普通数据访问属性并且属性描述符中writable没有被标记为false,那么就会直接在obj中添加一个a属性并赋值
  2. 如果原型链上层存在a,但是a的writable为false,那么obj中也无法被写入a属性,并且在严格模式下还会抛出一个错误
  3. 如果原型链上层存在a,并且它设置为了一个setter,那么a不会被添加到obj,也不会重新定义a这个setter,只会调用这个setter

如果需要在存在第二第三种情况下依旧屏蔽原型链上的a属性,那么不能使用=操作符来赋值,需要用Object.defineProperty(..)来添加这一属性

似乎JavaScript这一机制很让人难以理解,为什么一个对象会因为另一个对象有一个属性而没办法创建一个同名的属性呢

其实这么做是为了模拟类属性的继承,你可以吧原型链上层中的属性看做父类的属性,他会被obj继承(复制)

这么一来obj中的a属性也是只读,所以无法被赋值,也无法被创建

但是一定要注意,事实上JavaScript是不存在类似的继承复制的!举这个例子只是为了方便理解!事实上发生的只是使用[[Prototype]]关联到了另一个对象而已!

现在你可能会很好奇,为什么要关联到另一个对象,这样做有什么好处?

首先我们要明确一个概念,JavaScript中不存在类!

其他面向类的语言会用类来作为对象的抽象模式或者说蓝图,来描述对象的行为

但是JavaScript中只有对象,由对象自己来定义自己的行为

"构造函数"

new这个关键词很容易让人误解Foo是一个"类"

调用 new Foo() 创建的每个对象都将最终被[[Prototype]]这个属性链接到"Foo.prototype"这个对象

		function Foo() {
			this.name = "a";
		}
		var a = new Foo();
		console.log(
			Object.getPrototypeOf(a) === Foo.prototype //true
		)

在介绍this的绑定规则的时候我们介绍过new操作符的四个步骤

  1. 创建一个对象
  2. 设定这个对象的[[prototype]]
  3. 将函数中的this绑定到这个对象
  4. 如果函数中没有返回其他对象,那么把这个对象返回

其中第二步设定这个对象的[[prototype]]其实就是将a的[[prototype]]链接到了Foo.prototype指向的对象

在面对类的语言中,类可以被复制(实例化)很多次,就像用模具制作东西一样

但是在JavaScript中没有类似的复制机制,你不能创建一个类的多个实例

你只能创建多个对象,而他们的[[prototype]]链接的是同一个对象

比如上面例子中的 var a= new Foo()

new Foo()创建了一个新对象,这个对象的内部链接[[prototype]]关联到了Foo.prototype对象(Object.create(..)是直接关联到对应对象)

我们没有从"类"中复制任何一个行为到一个对象中,我们只是把两个对象互相关联

(事实上new Foo()这个函数调用实际上没有直接创建关联,这是一个间接的行为,但是Object.create(..)是直接创建的关联)

除了"构造函数"这个语义以外,Foo.prototype还有一个很迷惑人的属性 .constructor

Foo.prototype默认(声明时)有一个公有且不可枚举的属性.constructor

		function Foo() {
			this.name = "a";
		}
		var a = new Foo();
		console.log(
			Foo.prototype.constructor === Foo, //true
			a.constructor === Foo //true
		)

这个属性引用的是对象关联的函数(本例中是Foo),似乎表示"创建这个对象的函数"(但是这是错误的)

看起来似乎a.constructor===Foo意味着a有一个指向Foo的.constructor属性

但事实上a并没有这个属性,在介绍[[Get]]操作的时候我们说过,[[Get]]操作存在一个遍历原型链的行为

a.constructor只是通过默认的[[Prototype]]委托指向Foo,这跟"构造"毫无关系

举例来说,Foo.prototype的.constructor属性指向Foo只是声明时的默认属性,如果你创建了一个新对象并替换掉了默认的.prototype引用

那么新对象并不会自动改变并获取.constructor属性

		function Foo() {
			this.name = "a";
		}
		Foo.prototype = {};
		var a = new Foo();
		console.log(
			a.constructor === Foo, //false
			a.constructor === Object //true
		)

这个例子中Foo.prototype被指向了一新对象,这个对象中并没有.constructor属性,所以它会继续委托,一直到顶端的Object.prototype

所以事实上它调用的是Object.prototype.constructor属性,指向内置的Object()函数

事实上Foo.prototype对象有一个.constructor属性默认指向一个函数(Foo),这个函数也有一个叫做.prototype的引用指向这个对象

仅此而已,并不代表着constructor所指向的函数构造了这个对象

猜你喜欢

转载自blog.csdn.net/Aproducer/article/details/82705031