JavaScript基础-原型

134 阅读6分钟

JavaScript基础-原型

1. [[Prototype]]

JavaScript对象有一个特殊[[Prototype]]内置属性,其实就是对于其他对象的引用

var myObj = {
	a:2
}
myObj.a; // 2

对于默认[[Get]]操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它,但是如果a不在myObj中,就会需要使用对象的[[Prototype]]链

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

in操作符来检查属性是否在对象中存在,同样会查找对象的整条原型链(无论对象是否被枚举)

var anotherObj = {a:2};
// 创建一个关联到anotherObj的对象
var myObj = Object.create(anotherObj)

1.1 Object.prototype

哪里是[[Prototype]]的尽头,最终是会指向内置的Object.prototype,会包含很多通用的功能,比如说.toString()和.valueOf(),.hasOwnPrototype()

1.2 属性设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值,讲解一下过程

myObj.foo = "bar";

**
如果foo存在myObj对象中,并且是普通数据访问属性
赋值语句只会修改已有的属性值

如果foo不存在myObj对象中,也不存在[[Prototype]]链中
foo会直接添加到myObj对象中

如果foo不直接存在myObj中,而是在原型链的上层

  • 在原型链上foo没有标记是只读状态的,那么会直接在myObj添加一个foo的新属性,它是屏蔽属性
  • 在原型链上foo标记是只读状态,那么无法修改已有属性或者在myObj上创建屏蔽属性,总之不会发生屏蔽
  • 在原型链上foo是个setter,那么一定会调用setter,foo不会添加到(或者说屏蔽于)myObj


上面这些限制都是存在于=操作符赋值,使用Objec.defineProperty(...)并不受影响,通常来说,使用屏蔽得不偿失,所以应当尽量避免使用

隐式产生屏蔽,很危险一定要当心

var anotherObj = {a:2};
var myObj = Object.create(anotherObj);
anotherObj.a; // 2
myObj.a;// 2
another.hasOwnPrototype("a"); // true
myObj.hasOwnPrototype("a"); // false

myObj.a++;
anotherObj.a;// 2
myObj.a;// 3
myObj.hasOwnPrototype("a"); // true

实际这个过程,就是++操作是myObj.a = myObj.a + 1,因此++操作是首先会通过[[Prototype]]链查找属性a,并从anotherObj.a获取当前属性值2,然后给这个值加1,接着用[[put]]将值3赋给myObj中新建的屏蔽属性a

修改委托属性的时候一定要小心,唯一可靠点的是修改源对象属性anotherObj.a++

2. “类”

在JavaScript中,类无法描述对象的行为(因为根本不存在类!!),对象直接定义自己的行为,再说一遍,JavaScript中只有对象

2.1 “类”函数

模仿类,一直都是JavaScript使用构造函数

function Foo() {...}
var a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype;// true

实际上,我们并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联

new Foo()这个函数调用实际并没有直接创建关联,只是间接完成了我们的目标:一个关联到其他对象的新对象

有没有更直接的方法??有!!就是Object.create(...)

关于名称
什么名称就是原型继承,这个是专属于JavaScript语言里面使用的

JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问到另一个对象的属性和函数,也算是关联机制

2.2 “构造函数”

function Foo() {/* ...*/}
Foo.prototype.contructor=== Foo;// true
var a = new Foo();
a.contructor === Foo

.contructor这个属性引用的是对象关联的函数,指向的是“创建这个对象的函数”

只要使用了new关键字去调用的函数,都会被劫持变成“构造函数”

2.3 技术

绞尽脑汁想要模仿类的行为

function Foo(name) {
	this.name = name;
}
Foo.prototype.myName=function(){
	return this.name;
}
var a = new Foo("a");
var b = new Foo("b");

a.myName(); // "a"
b.myName(); // "b"

原理是,在创建过程中,a和b的内部[[Prototype]]都会关联到Foo.prototype上,当a和b中无法找到myName时,它会(通过委托)在Foo.prototype上面去找

.constructor引用和上面的同理,同样被委托在Foo.prototype,而Foo.prototype.contructor默认指向Foo

总的来说,a.constructor的值是直接可以被修改的,非常不可靠并且不安全的引用,尽量避免使用这些引用

3. (原型)继承

function Foo(name){
  this.name=name;
}
Foo.prototype.myName=function(){
  return this.name;
}

function Bar(name, label){
	Foo.call(this, name);
  this.label = label;
}
// 创建了一个新的Bar.prototype对象并关联到Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.myLabel = function(){
	return this.label;
}

var a= new Bar("a", "obj a ")
a.myName();// a
a.myLabel();// obj a

上面👆这种就是典型的“原型风格”,和下面两种代码方式是错误的❌,并且下面👇的会存在一些问题

// 使用=操作符直接赋值
// 直接引用了Foo.prototype,但是执行添加.myLabel会直接修改了Foo.prototype对象本身
// 如果只这样子不如直接使用Foo
Bar.prototype = Foo.prototype

// 基本上会满足你的需求,但可能产生一些副作用
// 确实会创建一个关联到Bar.prototype的新对象
// 但是使用了“构造函数的调用”,导致(修改状态,注册到其他对象,给this添加数据属性)
// 就是影响Bar的后代
Bar.prototype = new Foo();

推荐修改关联的ES6的方法,Object.setPrototypeOf()更加标准可靠

// 直接修改现有的Bar.prototype的关联
Object.setPrototypeOf(Bar.prototype, Foo.prototype)

3.1 检查“类”关系

找出继承的祖先

instanceof

function Foo(){
	// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
// 在a的整条[[prototype]]链中是否有指向Foo.prototype的对象
a instanceof Foo;// true

所以,这也是instanceof的局限性,这个操作符左边👈是对象,右边👉是函数,只有存在对象和函数的prototype关系才行

.isPrototypeOf(...)

Foo.prototype.isPrototype(a)

只需要两个对象判断关系,前者是否出现在后者的[[prototype]]链中

也可以直接获取原型链来做比较

Object.getPrototypeOf(a) === Foo.prototype; // true

也还有一种非标准的方法来访问

a.__proto__ === Foo.prototype;

4. 对象关联

4.1 创建关联

为什么要模拟类?和使用[[Prototype]]机制的意义是什么?

了解Object.create这个大英雄

var foo={
	something: function(){
  	console.log("tell me something good...")
  }
}
// 创建一个新对象,和关联到指定对象
var bar = Object.create(foo);
bar.something(); // "tell me ..."

这个api可以充分发挥[[Prototype]]机制的威力(委托),并且避免了不必要的麻烦,比如使用new的构造函数调用生成.prototype和.constructor引用

Object.create()的polyfill代码,因为是es5新增的

if(!Object.create){
  	Object.create=function(o){
    	function F(){}
      // 关联到指定对象
      F.prototype = o;
      // 返回新的对象
      return new F();
    }
 }	

4.2 关联关系是备用

var anotherObj = {
	cool:function() {
  	console.log("cool")
  }
}
var myObj = Object.create(anotherObj);
myObj.doCool = function(){
  // 内部委托
	this.cool();
}
myObj.doCool(); //"cool"

内部委托比起直接委托可以让api接口设计更加清晰