1 - 原型是什么?
1.1 - 原型指的是原型属性,原型也是一个对象。
- 原型指的是两个原型属性:
- prototype: 显示原型属性
- _proto_: 隐式原型属性
- 所有函数都有显示原型属性。 它的值叫做原型对象,这个原型对象默认有两个属性:
- constructor 指向函数本身
- _proto_ 指向Object.prototype
- 特殊: Object.prototype._proto_ === null 这个就是原型链尽头
- 注意:箭头函数: 没有 显示原型 属性,不能被new调用 ,只有隐式原型。
- 所有对象都有隐式原型属性。
- 对象的隐式原型属性的值指向其构造函数显示原型属性的值
- 如果这个普通对象是你 new 的,构造函数就是你 new 的函数
- 如果不是,就是 new Obejct 产生的,所以构造函数就是 Object
- 所有函数都是 new Function 产生的,包括它自己 Function
- 所有数组都是 new Array 产生的
<script type="text/javascript">
/*
<<1>> 原型是什么? 原型指的是原型属性:
prototype: 所有函数都有prototype属性, prototype属性的值是对象。这个对象叫原型对象。
这个原型对象上默认有两个属性:
constructor: 指向函数自己本身
__proto__: Object.prototype __proto__的值默认指向它的大写的Object的原型属性(原型对象)上
__proto__: 所有实例对象都有__proto__属性,它的值的是对象。
值为它对应构造函数.prototype的值
*/
// 下面我们来验证一下,上面说的这些结论:
// 原型只有函数有,其他的数组对象都没有
// 我们用最常见的两个典型函数,来打印输出,发现值都是对象,
// 而且对象里还有prototype属性:
//1 - 用js内置的函数:Object
console.log(Object.prototype); //Object
//2 - 用我们随便写的一个函数:person
// 我们自己定义的函数,只会有两个属性:constructor和__proto__属性
function person() { };
console.log(person.prototype);
// 打印结果:是一个对象,而且只有两个属性
// Object
//constructor: ƒ person()
//__proto__: Object
// 接下来,我们看一下person这个显示原型上的constructor属性的值 是不是等于 person 这个函数自己自身呢?
console.log(person.prototype.constructor === person); //true 说明constructor: 指向的是函数自己本身
//原型链尽头是null (不用深究,是内置函数帮我们定义封装好的,这个只需要记结论即可)
console.log(Object.prototype.__proto__); // null
// 问题:现有内置函数Object.prototype属性呢,还是先有person.prototype.__proto__属性呢?
// 答案是:
// 先有的这个内置函数:Object.prototype
// 才有的 person.prototype.__proto__
// 因为js有个特点:js在执行之前,Object和array这些就已经创建好了,
// js都是这些内置的函数,数组,对象等创建好之后,才开始执行的我们所写的代码:
// 所以像 这些 内置的函数的话,都会放一些自己的方法。(即自带一些方法)
console.log(person.prototype.__proto__ === Object.prototype); //true
// 接下来验证的是:除了函数之外,其他的类型不含有原型属性:
// 简单举例两个看看吧:
var obj = {};
var arr = [];
console.log(obj.prototype); //undefined 证明对象没有原型属性
console.log(arr.prototype); //undefined 证明了数组也没有原型属性
// 2 - 现在来验证这些结论:
// __proto__: 所有实例对象都有__proto__属性,它的值的是对象。
// __proto__ 的值:为它对应构造函数.prototype的值
// 虽然我们平时生活中,都是把new 出来的,才叫做实例对象,
// 但是事实上:所有对象都是实例对象(只不过我们看不到过程罢了)
var obj = {}; //其实这个对象是 new Object() 所产生的,只是我们没有看到这个过程罢了
var array = []; // new Array()
var fn = function () { }; // new Function()
// 打印的结果是:这三个对象类型的,都有值,说明,都含有__proto__属性
console.log(obj.__proto__);
console.log(array.__proto__);
console.log(fn.__proto__);
console.log(obj.__proto__ === Object.prototype); //true
console.log(array.__proto__ === Array.prototype); //true
console.log(fn.__proto__ === Function.prototype); //true
//写一个Person 的函数
function Person() {
}
var p = new Person();
console.log(p.__proto__ === Person.prototype); //true
1.2 - 显示原型和隐式原型
- 每个函数function都有一个prototype,即显式原型
- 每个实例对象都有一个_proto_,可称为隐式原型
- 对象的隐式原型的值为其对应构造函数的显式原型的值
- 内存结构(图)
- 总结:
-
函数的prototype属性: 在定义函数时自动添加的, 默认值是一个对象
-
对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
-
程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前),
-
2015年就推出的版本,ES6之后就可以了,直接操作了隐式原型了。
1.2.1 - new关键字到底做了什么:
-
调用了函数
-
建了实例对象。并将函数的this指向实例对象
-
实例对象._proto_ = 构造函数.prototype (即构造函数上的所有的东西,我们都可以拿到了)
(即所有的实例对象都可以通过继承来得到构造函数上的显示原型的所有的东西) -
返回实例对象(如果函数返回值是对象,就返回这个对象,如果不是,返回实例对象)
<script type="text/javascript">
/*
new关键字:
1. 调用了函数
2. 创建了实例对象。并将函数的this指向实例对象
3. 实例对象.__proto__ = 构造函数.prototype (即构造函数上的所有的东西,我们都可以拿到了)
(即所有的实例对象都可以通过继承来得到构造函数上的显示原型的所有的东西)
4. 返回实例对象(如果函数返回值是对象,就返回这个对象,如果不是,返回实例对象)
*/
function Person(name, age) {
this.name = name; // p.name= name; p 中添加属性name,值为name
this.age = age;
// 如果写了下面这个具体的返回值对象的话,就会返回这个:
// 返回一个对象,一般都不会这么写:
// return {a:123}
}
// 实例对象.__proto__ = 构造函数.prototype
// 因为 实例对象的隐式原型属性 等于 构造函数的显示原型属性
Person.prototype.setName = function (name) {
this.name = name;
};
// 打印结果里的P1和p2 都有自身属性:age 和 name 属性和值
// 因为我new的时候,会调用函数,并且把this指向实例对象P1,
// 就会把this.name = name;改为: p.name= name; p 中添加属性name,值为name
// 所以 p身上就会有name和 age的直接属性;
// 而且setName() 方法,就是会在p 身上的隐式原型属性身上
// 所以this就等于是实例对象:
// 那我创建多个函数。他们指向的还是同一个原型对象,这样就可以更加的复用代码
var p1 = new Person('jack', 18);
var p2 = new Person('jack', 18);
console.log(p1, p2);
//Person
// age: 18
// name: "jack"
</script>
</body>
</html>
1.3 - 原型链
- 原型链(图解)
- 访问一个对象的属性时,
- 先在自身属性中查找,找到返回
- 如果没有, 再沿着__proto__这条链向上查找, 找到返回
- 如果最终没找到, 返回undefined
- 别名: 隐式原型链
(因为我们的隐式原型和显示原型是没有关系,虽然值是同个值,但是其实看起来是没有任何关系,
原型链是由隐式原型属性构成的一种结构,它永远不会看显示原型,所以才又取了个名字:隐式原型链) - 原型链的作用: 就是用来查找对象的属性或方法
<script type="text/javascript">
function Person(name) {
this.name = name;
}
// 这里用了原型 来替代属性,但是一般平时我们不用,
// 在这里只是为了更好的来理解原型链的用法:
Person.prototype.age = 18;
// 创建一个 p 的实例对象:
var p = new Person('jack');
console.log(p);
// {
// name: 'jack',
// __proto__: { 隐式原型的值
// age: 18,
// constructor: Person, 这个值指向函数自己本身
// __proto__: object.prototype 这个值指向函数原型
// }
console.log(p.name, p.age) //jack 18
//p.name:通过这个p找到P这个对象:然后找到name属性,返回它的值
//p.age :先沿着自身属性上找,发现没有age属性,然后就会沿着隐式原型属性在找,发现找到了age属性,
// 然后我们就着上面的案例,再增加些属性代码:
// 案例二:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.age = 18;
var p = new Person('jack', 20);
/*
现在的实例对象长成:这样:
{
name: 'jack',
__proto__: {
age: 18,
constructor: Person, 这个值指向函数自己本身
__proto__: {
toString: fn,
valueOf: fn,
hasOwnProperty: fn,
__proto__: null --> 原型链的尽头: Object.prototype.__proto__ === null
(即原型链尽头,存在哪里呢?存在大写的Object的显示原型上面的隐式原型上的值等于null)
}
}
}
*/
console.log(p.name, p.age, p.sex); //jack 20 undefined
// 上诉所讲的这些规则就是原型链,由多个隐式原型来组成这样的结构,这样的叫原型链,
// 就是有个对象,对象里面有个隐式原型属性__proto__,然后隐式原型属性__proto__又指向一个对象,
// 然后这个对象也有一个隐式原型属性,又会指向另一个对象.就是这样的一层层的指向关系。
// 然后我们找属性的话,会先在自身里面找, 找不到的话,再沿着隐式原型属性__proto__再接着找,
// 再找不到,还会沿着隐式原型属性__proto__接着找,直到找到值,找到返回。如果最终没找到,找到的就会是原型链的尽头:__proto__: null ,就会返回undefined
// 所以原型链的作用:
// 就是对象查找属性的规则,即对象查找属性的规则会沿着原型链找,自身找不到,也可以沿着自身属性接着找,如果找到就会返回,
// 如果找不到,就会找到原型链的尽头Object.prototype.__proto__ === null,返回值为undefined。
</script>
</body>
</html>
1.4 - 原型图:
1.4.1 - 拆分出来的原型图
- 第一张图:
这是一个构造函数,叫Foo
另外一个构造函数,是大写的Object
所有函数都有一个显示原型属性prototype,
然后显示原型属性的值 是不是指向 原型对象Foo.prototype
然后原型对象Foo.prototype 里面有一个constructor 属性 指向了函数的本身
- 第二张图:
- 隐式原型属性指向了大写的Object的显示原型属性
- 你new出来实例对象,构造函数就是你new的那个函数。
- 不是你new出来的实例对象,默认就是new Object产生的, 所以构造函数就是Object
下面:一样,所有的函数都有显示原型,指向了它的原型对象,
然后原型对象就默认有两个属性,一个constructor指向自己本身。
还有一个隐式原型值为null
实例对象的隐式原型属性指向的是其构造函数的显示原型属性。
首先这是一个对象,对象里面有隐式原型
构造函数
这个是构造函数的显示原型
- 第三张:说明这个显示原型对象的构造函数,就是这个大写的Object
-
第四张: f1 和 f2 就是 new Foo()产生出来的实例对象,
然后实例对象都有隐式原型属性,它的值都是指向显示原型的值
-
第五张:所有的普通对象都是new Object 产生的 ,所以普通对象也有隐式原型属性,
指向其构造函数的显示原型属性。
-
第六张 function Foo()函数是怎么产生的?
这句话就相当于是 var Foo = new Function
所以大写的Fun是构造函数
Foo是实例对象,
那么自然就有个结论:
实例对象的隐式原型的值指向构造函数显示原型属性的值,
-
第七张图:
-
现在有个大写的Object,首先函数也是对象,所以这个函数也会拥有对象的特性,
- 所以函数肯定也是new 某个东西 产生的
所有函数都是大写的new Function产生的,包括它自己
所以,Object也是new Function 产生的。
-
其实大写的Array和String也都是new Function 产生的,
-
所以这些大写的构造函数都有这个特点:有一个隐式原型属性,
-
第八张图:
-
但是上面这个是个例外,大写的Function可以看做是new自己产生的,
(所以这个大写的Function即是构造函数又是实例对象)
-
所以,实例对象的隐式原型又指向构造函数的显示原型,
-
因为大写的Function即是构造函数又是实例对象,所以就会指向同一个对象。
正常的隐式原型和显式原型不是指向同一个对象,
-
-
第九张图:
-
即所有函数都有显示原型prototype属性
-
因为这个对象就是new Object 产生的,因为不是你new出来的对象,就都是new Object产生的,
1.4.2 - 完整的原型图:
总结的结论:
- 对象 都有隐式原型
- 对象的隐式原型就看它的构造函数是谁,构造函数就看如果是对象,你new的就是你new的那个函数,不是你new的就是new Object 产生的。
- 所有函数 都有 显示原型prototype属性
- 如果是函数,就是new Function 产生的,所以函数都有隐式原型,都是指向大写的Function的显示原型。
- 即所有的函数都是new Function产生的,包括Function
- Function._proto_ === Function.prototype
- 原型对象又都指向原型对象:
- 然后这个原型对象上默认有两个属性:
- constructor: 指向函数自己本身
- __proto__: Object.prototype
- (__proto__的值默认指向它的大写的Object的原型属性(原型对象)上)
- _proto_: 所有实例对象都有__proto__属性,它的值的是对象,值为它对应构造函数.prototype的值
1.5 - 原型读取对象和设置对象属性值需要注意的地方
接下来我们研究一下,原型链的一个小小的例子:
注意:
-
读取 对象的属性值时: 会自动到原型链中查找
-
设置 对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
-
方法 一般定义在原型中, 属性 一般通过构造函数定义在对象本身上
<script type="text/javascript">
function Person(name, age) {
// 添加属性一般都是在这里面写的:
this.name = name;
this.age = age;
}
// 思考问题:为什么我们往往会把方法放在原型上,而不把属性放在原型上呢?
// (属性一般是写在对象里面,方法写在外面)
// 一般我们只会在这里添加方法,
// 方法往往都是为了复用,往往都是公共的,所以会放在原型上
// 这样不管我们创建多少个实例对象,实例对象的隐式原型都是指向这个构造函数的显示原型,
// 而我们的原型始终还是只有这一个,
// 而隐式原型通过原型对象的方式,就能得到显示原型上的这个setName方法
// 所以从头到尾的setName() 方法,就只会有一份,这样就大大提高效率,节省空间
// 原型上就是要放一些公共的东西
Person.prototype.setName = function (name) {
this.name = name;
};
// 一般我们都不会这么写属性的,这里是为了解释案例使用的
// 因为属性一般都是不一样的,达不到复用的目的
// 而且写在这里的话,属性都写死了,不会变化
Person.prototype.sex = '男';
// 我们创建一个实例对象,并且传参:
// 这里不管我们创建多少个实例对象,实例对象的隐式原型都是指向这个构造函数的显示原型,
var p = new Person('jack', 18);
/*
{
name: 'jack',
age: 18,
__proto__: {
setName: fn,
sex: '男'
}
}
*/
// 只有读取属性的时候,才会查找原型链
console.log(p);
// 设置对象的属性值时: 是不会从原型链上查找的,
// 自身有就有,没有就添加一个,
// 自身有就会修改原来的值,
// 自身没有这个属性,则会添加一个,并设置值
// 接着我们修改:
p.age = 20; //自身有就会修改原来的值,
p.sex = '女'; //这个是自身没有的属性,则会添加一个,
/*
{
name: 'jack',
age: 20,
sex: '女',
__proto__: {
setName: fn,
sex: '男'
}
}
*/
// 会先在自身属性上找,找到之后,原型就不看了
console.log(p.sex); //女
</script>
</body>
</html>