>웹 프론트엔드 >JS 튜토리얼 >me_javascript 기술로 자바스크립트 프로토타입과 프로토타입 체인을 배워보세요

me_javascript 기술로 자바스크립트 프로토타입과 프로토타입 체인을 배워보세요

WBOY
WBOY원래의
2016-05-16 15:31:411364검색

JavaScript를 사용해 본 학생들은 프로토타입에 대해 잘 알고 있을 텐데, 초보자들은 함수에 프로토타입 속성이 있다는 것과 인스턴스 액세스를 위해 함수를 추가할 수 있다는 것만 알고 있습니다. 최근 일부 고급 JavaScript 프로그래밍을 살펴보고 마침내 그 미스터리를 공개했습니다.

각 함수에는 객체에 대한 참조인 프로토타입 속성이 있습니다. 이 객체를 프로토타입 객체라고 합니다. 프로토타입 객체에는 함수 인스턴스가 공유하는 메서드와 속성이 포함되어 있습니다. 즉, 함수가 생성자 호출( new 연산자를 사용하여 호출하면 새로 생성된 객체는 프로토타입 객체의 속성과 메서드를 상속받습니다. 전통적인 객체 지향 언어와 달리 Javascript의 상속 메커니즘은 클래스 클래스가 아닌 프로토타입을 기반으로 합니다.

1. 프라이빗 변수 및 함수
프로토타입에 대해 자세히 이야기하기 전에 프로토타입의 디자인 의도를 더 잘 이해할 수 있도록 몇 가지 관련 사항에 대해 이야기해 보겠습니다. JavaScript 프로토타입 체인을 이해하기 전에 먼저 JavaScript 범위 체인을 이해하는 것이 필요합니다. JavaScript의 함수 범위에서 함수 내에 정의된 변수와 함수가 외부 세계에 대한 인터페이스를 제공하지 않으면 외부에서 접근할 수 없습니다. 즉, 프라이빗 변수와 프라이빗 함수가 됩니다.

function Obj(){
 var a=0; //私有变量
 var fn=function(){ //私有函数

 }
}

이런 방식으로 변수 a와 함수 fn은 함수 개체 Obj 외부에서 액세스할 수 없습니다. 이들은 비공개가 되어 Obj 내부에서만 사용할 수 있습니다. 심지어 함수 Obj의 인스턴스도 이러한 변수와 함수에 액세스할 수 없습니다.

var o=new Obj();
console.log(o.a); //undefined
console.log(o.fn); //undefined

2. 정적 변수 및 함수

함수가 정의되고 "."로 추가된 속성과 함수는 객체 자체를 통해 계속 액세스할 수 있지만 해당 인스턴스에 액세스할 수 없는 경우 이러한 변수와 함수를 각각 정적 변수라고 합니다. Java와 C#을 사용하면 static의 의미를 쉽게 이해할 수 있습니다.

function Obj(){}
 Obj.a=0; //静态变量
 Obj.fn=function(){ //静态函数 
}

console.log(Obj.a); //0
console.log(typeof Obj.fn); //function

var o=new Obj();
console.log(o.a); //undefined
console.log(typeof o.fn); //undefined

3. 인스턴스 변수, 함수

객체 지향 프로그래밍에서는 일부 라이브러리 함수 외에도 객체가 정의될 ​​때 일부 속성과 메서드를 정의하기를 희망하며, JavaScript는 인스턴스화 후에 액세스할 수도 있습니다.

function Obj(){
 this.a=[]; //实例变量
 this.fn=function(){ //实例方法 
 }
}

console.log(typeof Obj.a); //undefined
console.log(typeof Obj.fn); //undefined

var o=new Obj();
console.log(typeof o.a); //object
console.log(typeof o.fn); //function

그러나 위의 목적은 달성할 수 있습니다

function Obj(){
 this.a=[]; //实例变量
 this.fn=function(){ //实例方法

 }
}

var o1=new Obj();
o1.a.push(1);
o1.fn={};
console.log(o1.a); //[1]
console.log(typeof o1.fn); //object
var o2=new Obj();
console.log(o2.a); //[]
console.log(typeof o2.fn); //function

위 코드의 실행 결과는 예상과 완전히 일치하지만 o1에서는 a와 fn이 수정되었지만 배열과 함수는 모두 객체이자 참조 유형이므로 문제가 있음을 보여줍니다. , 이는 o1의 속성 및 메서드가 o2의 속성 및 메서드와 이름이 동일하더라도 참조가 아니라 Obj 개체에서 정의한 속성 및 메서드의 복사본임을 의미합니다.

이것은 속성에는 문제가 되지 않지만 메소드에는 큰 문제가 됩니다. 왜냐하면 메소드가 정확히 동일한 기능을 수행하지만 두 번 복사되기 때문입니다. 함수 객체에 수천 개의 인스턴스 메소드가 있는 경우 각 인스턴스는 분명히 비과학적인 수천 가지 방법의 복사본을 유지해야 합니다. 프로토타입이 탄생하게 된 이유는 무엇입니까? 먼저 객체의 의미를 살펴보겠습니다.

4. 일반 객체와 함수 객체

JavaScript에서는 모든 것이 객체입니다! 하지만 대상도 다릅니다. 일반 객체와 함수 객체로 구분하면 Object와 Function은 JS에 포함된 함수 객체입니다. 다음은 몇 가지 예입니다

function f1(){};
 var f2 = function(){};
 var f3 = new Function('str','console.log(str)');

 var o3 = new f1();
 var o1 = {};
 var o2 =new Object();

 console.log(typeof Object); //function
 console.log(typeof Function); //function
 console.log(typeof o1); //object
 console.log(typeof o2); //object
 console.log(typeof o3); //object
 console.log(typeof f1); //function
 console.log(typeof f2); //function
 console.log(typeof f3); //function 

위의 예에서 o1 o2 o3은 일반 객체이고, f1 f2 f3은 함수 객체입니다. 구별하는 방법은 사실 매우 간단합니다. new Function()을 통해 생성된 모든 객체는 함수 객체이고, 나머지는 일반 객체입니다. f1, f2는 최종적으로 new Function()을 통해 생성됩니다. 함수 객체는 New Function()을 통해서도 생성됩니다.

5. 프로토타입 프로토타입

JavaScript에서는 객체(함수)가 정의될 ​​때마다 객체에 미리 정의된 속성이 포함됩니다. 함수 객체의 속성 중 하나는 프로토타입 객체입니다. 참고: 일반 객체에는 프로토타입이 없지만 _ proto _ 속성이 있습니다.

프로토타입 객체는 실제로는 일반 객체입니다(함수 객체이지만 매우 특수한 Function.prototype은 제외합니다. 프로토타입 속성이 없습니다(앞서 언급했듯이 함수 객체는 모두 프로토타입 속성을 가집니다)). 아래 예를 살펴보세요.

 function f1(){};
 console.log(f1.prototype) //f1{}
 console.log(typeof f1.prototype) //Object
 console.log(typeof Function.prototype) // Function,这个特殊
 console.log(typeof Object.prototype) // Object
 console.log(typeof Function.prototype.prototype) //undefined

이 console.log(f1.prototype) //f1 {}의 출력에서 ​​f1.prototype이 f1의 인스턴스 객체임을 알 수 있습니다(여기서는 f1의 프로토타입 객체가 있습니다). f1이 생성되면 인스턴스 객체가 생성되어 해당 프로토타입에 할당됩니다.

 var temp = new f1();
 f1. prototype = temp;

그래서 Function.prototype이 왜 함수 객체인지 이해하기 쉽습니다. 위에서 언급했듯이 new Function()에 의해 생성된 모든 객체는 함수 객체이므로 temp1은 함수 객체입니다.

var temp1 = new Function ();
 Function.prototype = temp1;

프로토타입 객체는 어떤 용도로 사용되나요? 주로 상속에 사용됩니다. 예를 들어보세요:

var person = function(name){
 this.name = name
};
person.prototype.getName = function(){
 return this.name; // 这里this指向原型对象person ==>person.name
}
var xpg = new person(‘xiaopingguo');
xpg.getName(); //xiaopingguo

从这个例子可以看出,通过给person.prototype设置了一个函数对象的属性,那有person实例(例中:xpg)出来的普通对象就继承了这个属性。具体是怎么实现的继承,就要讲到下面的原型链了。

在深入的讲一遍:无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性(同时它也是一个对象),默认情况下prototype属性(对象)会默认获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针,有些绕了啊,写代码、上图!

function Person(){

}

根据上图可以看出Person对象会自动获得prototyp属性,而prototype也是一个对象,会自动获得一个constructor属性,该属性正是指向Person对象。

当调用构造函数创建一个实例的时候,实例内部将包含一个内部指针(很多浏览器这个指针名字为_ proto _ )指向构造函数的prototype,这个连接存在于实例和构造函数的prototype之间,而不是实例与构造函数之间。

function Person(name){
 this.name=name;
}

Person.prototype.printName=function(){
 alert(this.name);
}

var person1=new Person('Byron');
var person2=new Person('Frank');

Person的实例person1中包含了name属性,同时自动生成一个_ proto _属性,该属性指向Person的prototype,可以访问到prototype内定义的printName方法,大概就是这个样子的:

写段程序测试一下看看prototype内属性、方法是能够共享

function Person(name){
 this.name=name;
}

Person.prototype.share=[];

Person.prototype.printName=function(){
 alert(this.name);
}

var person1=new Person('Byron');
var person2=new Person('Frank');

person1.share.push(1);
person2.share.push(2);
console.log(person2.share); //[1,2]

果不其然!实际上当代码读取某个对象的某个属性的时候,都会执行一遍搜索,目标是具有给定名字的属性,搜索首先从对象实例开始,如果在实例中找到该属性则返回,如果没有则查找prototype,如果还是没有找到则继续递归prototype的prototype对象,直到找到为止,如果递归到object仍然没有则返回错误。同样道理如果在实例中定义如prototype同名的属性或函数,则会覆盖prototype的属性或函数。—-这就是Javascript的原型链。

function Person(name){
 this.name=name;
}

Person.prototype.share=[];

var person=new Person('Byron');
person.share=0;

console.log(person.share); //0;而不是prototype中的[]

6.原型链

JS在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做_ proto _的内置属性,用于指向创建它的函数对象的原型对象prototype。以上面的例子

复制代码 代码如下:
console.log(xpg.__ proto __ === person.prototype) //true

同样,person.prototype对象也有_ proto _属性,它指向创建它的函数对象(Object)的prototype

复制代码 代码如下:
console.log(person.prototype.__ proto __=== Object.prototype) //true

继续,Object.prototype对象也有_ proto _属性,但它比较特殊,为null

复制代码 代码如下:
console.log(Object.prototype.__ proto __) //null

这个有_ proto _ 串起来的直到Object.prototype._ proto _为null的链叫做原型链。如下图:

原型链中属性查找:
当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined,我们来看一个例子:

 function foo() {
 this.add = function (x, y) {
 return x + y;
 }
}

foo.prototype.add = function (x, y) {
 return x + y + 10;
}

Object.prototype.subtract = function (x, y) {
 return x - y;
}

var f = new foo();
alert(f.add(1, 2)); //结果是3,而不是13
alert(f.subtract(1, 2)); //结果是-1

通过代码运行,我们发现subtract是安装我们所说的向上查找来得到结果的,但是add方式有点小不同,这也是我想强调的,就是属性在查找的时候是先查找自身的属性,如果没有再查找原型,再没有,再往上走,一直插到Object的原型上,所以在某种层面上说,用 for in语句遍历属性的时候,效率也是个问题。

还有一点我们需要注意的是,我们可以赋值任何类型的对象到原型上,但是不能赋值原子类型的值,比如如下代码是无效的:

function Foo() {}
Foo.prototype = 1; // 无效

7、构造函数、实例和原型对象的区别

实例就是通过构造函数创建的。实例一创造出来就具有constructor属性(指向构造函数)和proto属性(指向原型对象),

构造函数中有一个prototype属性,这个属性是一个指针,指向它的原型对象。

原型对象内部也有一个指针(constructor属性)指向构造函数:Person.prototype.constructor = Person;

实例可以访问原型对象上定义的属性和方法。

在这里person1和person2就是实例,prototype是他们的原型对象。

再举个栗子:

<script type="text/javascript">
 function Animal(name) //积累构造函数
 {
 this.name = name;//设置对象属性
 }

 Animal.prototype.behavior = function() //给基类构造函数的prototype添加behavior方法
 { 
 alert("this is a "+this.name);
 }

 var Dog = new Animal("dog");//创建Dog对象
 var Cat = new Animal("cat");//创建Cat对象

 Dog.behavior();//通过Dog对象直接调用behavior方法
 Cat.behavior();//output "this is a cat"

 alert(Dog.behavior==Cat.behavior);//output true;
</script>

8、原型的使用

原型使用方式1:
在使用原型之前,我们需要先将代码做一下小修改:

 var Calculator = function (decimalDigits, tax) {
 this.decimalDigits = decimalDigits;
 this.tax = tax;
 };

然后,通过给Calculator对象的prototype属性赋值对象字面量来设定Calculator对象的原型。

 Calculator.prototype = {
  add: function (x, y) {
  return x + y;
  },

  subtract: function (x, y) {
  return x - y;
  }
 };
 //alert((new Calculator()).add(1, 3));

我们就可以new Calculator对象以后,就可以调用add方法来计算结果了。

原型使用方式2:
第二种方式是,在赋值原型prototype的时候使用function立即执行的表达式来赋值,即如下格式:

复制代码 代码如下:
Calculator.prototype = function () { } ();

它的好处在前面的Item里已经知道了,就是可以封装私有的function,通过return的形式暴露出简单的使用名称,以达到public/private的效果,修改后的代码如下:
Calculator.prototype = function () {
 add = function (x, y) {
  return x + y;
 },

 subtract = function (x, y) {
  return x - y;
 }
 return {
  add: add,
  subtract: subtract
 }
} ();

//alert((new Calculator()).add(11, 3));

同样的方式,我们可以new Calculator对象以后调用add方法来计算结果了。

分步声明:
上述使用原型的时候,有一个限制就是一次性设置了原型对象,我们再来说一下如何分来设置原型的每个属性吧。

var BaseCalculator = function () {
 //为每个实例都声明一个小数位数
 this.decimalDigits = 2;
};
//使用原型给BaseCalculator扩展

BaseCalculator.prototype.add = function (x, y) {
 return x + y;
};

BaseCalculator.prototype.subtract = function (x, y) {
 return x - y;
};

声明了一个BaseCalculator对象,构造函数里会初始化一个小数位数的属性decimalDigits,然后通过原型属性设置2个function,分别是add(x,y)和subtract(x,y),当然你也可以使用前面提到的2种方式的任何一种,我们的主要目的是看如何将BaseCalculator对象设置到真正的Calculator的原型上。

var BaseCalculator = function() {
 this.decimalDigits = 2;
};

BaseCalculator.prototype = {
 add: function(x, y) {
 return x + y;
 },
 subtract: function(x, y) {
 return x - y;
 }
};

重写原型:

在使用第三方JS类库的时候,往往有时候他们定义的原型方法是不能满足我们的需要,但是又离不开这个类库,所以这时候我们就需要重写他们的原型中的一个或者多个属性或function,我们可以通过继续声明的同样的add代码的形式来达到覆盖重写前面的add功能,代码如下:

//覆盖前面Calculator的add() function 
Calculator.prototype.add = function (x, y) {
 return x + y + this.tax;
};

var calc = new Calculator();
alert(calc.add(1, 1));

这样,我们计算得出的结果就比原来多出了一个tax的值,但是有一点需要注意:那就是重写的代码需要放在最后,这样才能覆盖前面的代码。

9、hasOwnProperty函数

hasOwnProperty是Object.prototype的一个方法,它可是个好东西,他能判断一个对象是否包含自定义属性而不是原型链上的属性,因为hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。

// 修改Object.prototype
Object.prototype.bar = 1; 
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

只有 hasOwnProperty 可以给出正确和期望的结果,这在遍历对象的属性时会很有用。 没有其它方法可以用来排除原型链上的属性,而不是定义在对象自身上的属性。

但有个恶心的地方是:JavaScript 不会保护 hasOwnProperty 被非法占用,因此如果一个对象碰巧存在这个属性,就需要使用外部的 hasOwnProperty 函数来获取正确的结果。

var foo = {
 hasOwnProperty: function() {
 return false;
 },
 bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 总是返回 false

// 使用{}对象的 hasOwnProperty,并将其上下为设置为foo
{}.hasOwnProperty.call(foo, 'bar'); // true

当检查对象上某个属性是否存在时,hasOwnProperty 是唯一可用的方法。同时在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法,这将会避免原型对象扩展带来的干扰,我们来看一下例子:

// 修改 Object.prototype
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
 console.log(i); // 输出两个属性:bar 和 moo
}

我们没办法改变for in语句的行为,所以想过滤结果就只能使用hasOwnProperty 方法,代码如下:

// foo 变量是上例中的
for(var i in foo) {
 if (foo.hasOwnProperty(i)) {
 console.log(i); //moo
 }
}

这个版本的代码是唯一正确的写法。由于我们使用了 hasOwnProperty,所以这次只输出 moo。如果不使用 hasOwnProperty,则这段代码在原生对象原型(比如 Object.prototype)被扩展时可能会出错。

总结:推荐使用 hasOwnProperty,不要对代码运行的环境做任何假设,不要假设原生对象是否已经被扩展了

10、拓展

_ ptoto _属性

_ ptoto _属性(IE浏览器不支持)是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性constructor,通过这两个属性,就可以访问原型里的属性和方法了。

Javascript中的对象实例本质上是由一系列的属性组成的,在这些属性中,有一个内部的不可见的特殊属性——_ proto _,该属性的值指向该对象实例的原型,一个对象实例只拥有一个唯一的原型。

function Box(){ //大写,代表构造函数
 Box.prototype.name = "trigkit4";//原型属性
 Box.prototype.age = "21";
 Box.prototype.run = function()//原型方法
 { 
 return this.name + this.age + 'studying';
 }
}

var box1 = new Box();
var box2 = new Box();
alert(box1.constructor);//构造属性,可以获取构造函数本身,
   //作用是被原型指针定位,然后得到构造函数本身

_ proto _属性和prototype属性的区别

prototype是原型对象中专有的属性。
_ proto _ 是普通对象的隐式属性,在new的时候,会指向prototype所指的对象;
_ ptoto _ 实际上是某个实体对象的属性,而prototype则是属于构造函数的属性。_ ptoto _只能在学习或调试的环境下使用。

原型模式的执行流程

1.先查找构造函数实例里的属性或方法,如果有,就立即返回。
2.如果构造函数的实例没有,就去它的原型对象里找,如果有,就立即返回

原型对象的

function Box(){ //大写,代表构造函数
 Box.prototype.name = "trigkit4";//原型属性
 Box.prototype.age = "21";
 Box.prototype.run = function()//原型方法
 { 
 return this.name + this.age + 'studying';
 }
}

var box1 = new Box();
alert(box1.name);//trigkit4,原型里的值
box1.name = "Lee";
alert(box1.name);//Lee,就进原则

var box2 = new Box();
alert(box2.name);//trigkit4,原型的值,没有被box1修改

构造函数的

function Box(){   
 this.name = "Bill";
}

Box.prototype.name = "trigkit4";//原型属性
Box.prototype.age = "21";
Box.prototype.run = function()//原型方法
{ 
 return this.name + this.age + 'studying';
}

var box1 = new Box();
alert(box1.name);//Bill,原型里的值
box1.name = "Lee";
alert(box1.name);//Lee,就进原则

综上,整理一下:

function Person(){};

Person.prototype.name = "trigkit4";
Person.prototype.say = function(){
 alert("&#65532;Hi");
}

var p1 = new Person();//prototype是p1和p2的原型对象
var p2 = new Person();//p2为实例化对象,其内部有一个__proto__属性,指向Person的prototype

console.log(p1.prototype);//undefined,这个属性是一个对象,访问不到
console.log(Person.prototype);//Person
console.log(Person.prototype.constructor);//原型对象内部也有一个指针(constructor属性)指向构造函数
console.log(p1.__proto__);//这个属性是一个指针指向prototype原型对象
p1.say();//实例可以访问到在原型对象上定义的属性和方法

  • 构造函数.prototype = 原型对象
  • 原型对象.constructor = 构造函数(模板)

- 原型对象.isPrototypeof(实例对象) 判断实例对象的原型是不是当前对象。

以上就是本文的全部内容,希望对大家的学习有所帮助。

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.