js基础系列之【原型和原型链】
声明:形成本文的出发点仅仅是个人总结记录,避免遗忘,并非详实的教程;文中引用了经过个人加工的其它作者的内容,并非原创。学海无涯
引入问题
一般我们是这样写的:
(需求驱动技术,疑问驱动进步)
// 构造函数
function Obj(name) {
this.name = name;
} // 原型
Obj.prototype.sayName = function() {
return `say: ${this.name}`;
} // '实例'
var obj = new Obj('a');
// ’实例'的属性和方法
obj.name // 'a'
obj.sayName() // 'say: a'
我们都知道实例obj的属性’name‘【是实例自己的】,而实例的sayName方法是【所有实例共享的(?)】是从原型中查找的(?)
如此就引出一些问题:
1、在实例中没有定义的方法在原型中是怎么查找的?
2、实例中共享的方法如果在原型中修改了,会不会影响所有实例? 如果在某一个实例中重新定义了该方法会不会对其它实例造成影响?
问题2我们可以先测试一下,看看结果
// 创建另一个实例
var obj_b = new Obj('b'); // 在实例中修改从原型中查找到的共享方法sayName
obj.sayName = 'changed'; obj.sayName // 'changed'
obj_b.sayName // function() {...} // 直接修改原型中的sayName方法
Obj.prototype.sayName = 'change from proto'; obj.sayName // 'changed'
obj_b.sayName // 'change from proto'
总结: 1、修改实例中从原型中’继承‘(暂时用这个词)的属性和方法是不会影响到其它实例的
2、修改原型中的方法会立即反应到所有实例中(如果一个实例重新定义了自己的该同名方法,则不会);
结论有了,但为什么呢? next -->
对象‘特殊’的内置属性[[prototype]]
先说结论:
1、javascript中的对象有一个特殊的内置属性[[prototype]],这个内置属性表示和其它对象之间的关联(我们通俗点可以说成:对其它对象的引用);
2、几乎所有的对象在创建时[[prototype]]都会指向一个非空的对象(当然可以是空对象,而且这种对象很有用处)
3、当试图访问对象的一个属性时,会触发[[get]] 操作,默认的[[get]]操作会先检查对象本身是否有这个属性,如果有,则返回,如果没有就需要使用[[prototype]]进行查找了,如果在[[prototype]]中找到该属性,也同样会触发[[get]]操作
4、我们可能已经猜到了,和结论3类似,当试图对对象的一个属性重新赋值时,同样会现查找该属性值,如果本身有则设置本身的该属性,如果没有则沿着[[prototype]]去查找
5、[[prototype]]的尽头在哪? 答案是所有普通的[[prototype]]最终都会指向Object.prototype(js内置的对象, Object是一个内置函数 当然函数也是对象-_-)
OK,上代码:
var obj_pro = {
_pri_: 'e', // 模拟私有属性
a: 'a',
func() {},
};
Object.defineProperty(obj_pro, 'b', {
writable: false,
enumable: true,
configurable: true,
value: 'b',
});
Object.defineProperty(obj_pro, 'c', {
writable: true,
enumable: true,
configurable: false,
value: 'c',
});
Object.defineProperty(obj_pro, 'd', {
writable: true,
enumable: false,
configurable: true,
value: 'd',
});
Object.defineProperty(obj_pro, 'e', {
get() {
return this._pri_ + 'get';
},
set(val) {
this._pri_ = val + 'set';
},
});
var obj_son = Object.create(obj_pro); // Object.create的作用通俗点就是使obj_son的[[prototype]]指向obj_pro
obj_son.own = 'own'; // 读取属性
obj_son.own // 'own'
obj_son.a // 'a'
obj_son.b // 'b'
obj_son.c // 'c'
obj_son.d // 'd'
obj_son.e // '1get' 执行了[[get]]操作
obj_son.func // func() {} // 设置属性
obj_son.own = 'set own'; // 设置完成后: 'set own'
obj_son.a = 'set a'; // 设置完成后: 'set a'
obj_son.b = 'set b'; // 设置完成后: 'b'
obj_son.c = 'set c'; // 设置完成后: 'set c'
obj_son.d = 'set d'; // 设置完成后: 'set d'
obj_son.e = 'set e'; // 设置完成后: '1setget'
代码看起来有点多... 其实只是我们创建了很多属性而已。
经过上面代码中的一通赋值操作,obj_son自身的属性有: own、a、c、d、_pri_(这个是因为我们在设置e属性的时候执行了[[set]]操作,而[[set]]操作中的this执行了obj_son)
理一理: 两个对象: obj_pro obj_son 通过Object.create将obj_son 的[[prototype]] 指向了obj_pro,这样两个对象之间就有了内在的关联
我们在obj_pro中创建了a、b、c、d、e 5个属性; a是默认类型属性,b、c、d通过Object.defineProperty去定义了 三个属性特性,e是我们定义的存取器 ([[getter]]/[[setter]])obj_son.own是我们后来定义在它自身上面的属性,用于做一个区分
可以看到我们在对象obj_son上去重新赋值属性b和属性e时结果是不成功的,为什么呢?
有一个结论about属性设置和屏蔽:
当我们进行类似 myObj.foo = 'bar'这样的属性设置过程是这样的, 如果对象myObj自身包含名为‘foo’的属性,则修改已有属性,如果myObj自身和[[prototype]]中都存在‘foo’属性,则[[prototype]]中的属性就会被忽略;如果没有[[prototype]]就会被遍历;如果在[[prototype]]链中也没有‘foo’属性,则会在myObj对象自身上创建属性‘foo’并赋值;如果[[prototype]]中查找到‘foo’,则会有下面几种情况,而且有一些我们想不到的情况
1. 如果在[[Prototype]] 链上层存在名为foo 的普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在myObject 中添加一个名为foo 的新属性,它是屏蔽属性。
2. 如果在[[Prototype]] 链上层存在foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
3. 如果在[[Prototype]] 链上层存在foo 并且它是一个setter,那就一定会调用这个setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo 这个setter。
是不是很乱,很烦,哈哈这就是js(逃);
其实到现在我们所涉及的原型是很简单的,图:
图 1
结合这个简单的图我们上述总结的就是对象在属性读取和属性设置的时候发生的和其相关的对象之间的一些过程;
通过js中的‘类’来了解原型链
一般我们在日常开发中总是听到面向对象开发模式这样的词汇,那js中的‘面向对象’是什么样子的呢?
构造函数创建对象
// 构造函数
function Animal(weight) {
this.weight = weight;
} Animal.prototype.eat = function() {
console.log('eat');
} // 实例
var anim1 = new Animal(10);
anim1.weight //
anim1.eat // console -> 'eat'
似乎是回到了我们之前引入的问题当中了,但是经过有关[[prototype]]的分析,我们很容易就会形成这样一幅图:
图 2
实际上构造函数的唯一用途就是构建和初始化实例对象
构造函数在js中并不是一种特殊的函数,只有使用 new 操作符调用的函数才是构造函数,本质上构造函数和普通函数没什么区别
每一个函数在创建时都会有一个prototype属性指向一个对象,该对象默认有一个constructor属性指向该函数,这个对象就称为该函数的原型对象
这里需要了解的是 使用new 操作符调用函数时究竟发生了什么
'var instance = new Func()'
过程是:首先创建一个对象;将函数调用的this绑定至这个对象;将该对象的[[prototype]]指向Func的原型对象即: Func.prototype;执行Func函数的代码,如果函数Func没有显式的return一个对象(return;或者return非对象同)就返回最初创建的对象;到这里就是我们熟知的构造函数创建对象的过程;
如果函数Func显式得返回了一个对象(如果显式return this同之前的过程),那么之前创建的对象就会被丢弃,返回显式返回的对象;显式返回的对象的[[prototype]]就不会指向Func.prototype对象,所以永远也不要使用new 去调用这样的函数,因为会引起歧义;
以上我们就解释了 图 2所述的关系;
[[prototype]] 和 __proto__
我们总是说[[prototype]],那我们有没有办法去验证这个关系呢? 其实在某些浏览器中(例如chrome)中提供了一个__proto__属性作用基本等同于[[prototype]],这个属性并没有出现在标准中,所以,永远也不要在项目中使用
验证我们的观点:
anim1.__proto__ === Animal.prototype // true
更进一步,构造函数Animal是个函数当然也是对象,按照我们之前说的,每个对象在创建的时候都会将[[prototype]]指向另一个对象,Animal的[[prototype]](__proto__)指向哪里呢? 测试一下:
Animal.__proto__ === Function.prototype // true
Function.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ // null Animal.prototype.__proto__ === Object.prototype // true Object.__proto__ === Function.prototype // true
是不是有点乱? 看来是时候更新完善我们的图了:
个人观点: 我们姑且把对象和函数先分开看待,prototype是函数的属性;[[prototype]]/__proto__是对象和对象之间关系的纽带;constructor是对象的一个属性,与prototype指向相反(当然其实是不可靠的,实际开发中最好不要使用这个属性)
沿着蓝色线条我们可以清楚地看出一个原型组成的链条,这就是我们所说的原型链
合理推测:我们所创建的函数(包括所谓的构造函数)是通过Function构造函数创建的,创建的函数func的[[prototype]]/__proto__指向Function.prototype;
创建的所有对象都是通过Object构造函数创建的,创建的对象obj的[[prototype]]/__proto__指向Object.prototype(当然包括Function.prototype);
最终所有的对象(包括函数)的[[prototype]]链条最末端都指向了Object.prototype,而Object.prototype的[[prototype]]/__proto__最终都归向null (这是不是暗示我们万物终归尘土,代码敲到最后就是不敲代码, 哈哈哈哈开个玩笑)
来点复杂的
// 构造函数
function Animal(weight) {
this.weight = weight;
} Animal.prototype.eat = function() {
console.log('eat');
}; function Dog(name) {
this.name = name;
} Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.bark = function() {
console.log('wang wang');
}; var doggie = new Dog('da huang');
通过上图我们会发现Dog.prototype自身并没有constructor,这是不是漏掉了?测试一下
doggie.constructor // Animal
Dog.prototype.hasOwnProperty('constructor') // false
是的,并不是我们想象的那样,实例doggie的constructor并不是Dog,而是沿着原型链一直找到了 Animal.prototype.constructor,是因为我们重写了Dog.prototype!!!
所以通过constructor去查找构造函数的方法并不那么可靠,但是我们为什么要知道构造函数呢???
所以我们为了面向对象而创建的构造函数,通过构造函数创建实例(对象),模仿传统面向对象语言的继承,结果呢? 我们得到的其实还是一堆对象,构造函数在创建完对象之后好像并没有什么实际用途了。
js基础系列之【原型和原型链】的更多相关文章
- JS核心系列:浅谈原型对象和原型链
在Javascript中,万物皆对象,但对象也有区别,大致可以分为两类,即:普通对象(Object)和函数对象(Function). 一般而言,通过new Function产生的对象是函数对象,其他对 ...
- Js基础知识(二) - 原型链与继承精彩的讲解
作用域.原型链.继承与闭包详解 注意:本章讲的是在es6之前的原型链与继承.es6引入了类的概念,只是在写法上有所不同,原理是一样的. 几个面试常问的几个问题,你是否知道 instanceof的原理 ...
- js基础系列框架:JS重要知识点(转载)
这里列出了一些JS重要知识点(不全面,但自己感觉很重要).彻底理解并掌握这些知识点,对于每个想要深入学习JS的朋友应该都是必须的. 讲解还是以示例代码搭配注释的形式,这里做个小目录: JS代码预解析原 ...
- js基础系列之【作用域】
声明:形成本文的出发点仅仅是个人总结记录,避免遗忘,并非详实的教程:文中引用了经过个人加工的其它作者的内容,并非原创.学海无涯 什么是作用域? 作用域就是一套规则,用于确定在何处以及如何查找变量(标识 ...
- js基础系列框架图 (转载)
- JS基础-全方面掌握继承
前言 上篇文章详细解析了原型.原型链的相关知识点,这篇文章讲的是和原型链有密切关联的继承,它是前端基础中很重要的一个知识点,它对于代码复用来说非常有用,本篇将详细解析JS中的各种继承方式和优缺点进行, ...
- 基础系列(1)之干掉JavaScript变量作用域
今天去某顺公司面试,发现一些基础知识都不记得了,于是乎决定把js基础系列的全部梳理一遍,今天就整理下js变量作用域的相关基础知识点,配合最常遇到的笔试题阐述. 题一: var g = "a ...
- 前端总结·基础篇·JS(一)原型、原型链、构造函数和字符串(String)
前端总结系列 前端总结·基础篇·CSS(一)布局 前端总结·基础篇·CSS(二)视觉 前端总结·基础篇·CSS(三)补充 前端总结·基础篇·JS(一)原型.原型链.构造函数和字符串(String) 前 ...
- JS基础-该如何理解原型、原型链?
JS的原型.原型链一直是比较难理解的内容,不少初学者甚至有一定经验的老鸟都不一定能完全说清楚,更多的"很可能"是一知半解,而这部分内容又是JS的核心内容,想要技术进阶的话肯定不能对 ...
随机推荐
- 拒绝服务(DoS)理解、防御与实现
一.说明 我一直不明白为什么拒绝服务,初学着喜欢拿来装逼.媒体喜欢吹得神乎其神.公司招聘也喜欢拿来做标准,因为我觉得拒绝服务和社会工程学就是最名不副实的两样东西.当然由于自己不明确拒绝服务在代码上是怎 ...
- 解决SpringMVC+Thymeleaf中文乱码
乱码效果截图 解决办法:在org.thymeleaf.templateresolver.ServletContextTemplateResolver和org.thymeleaf.spring5.vie ...
- static_assert enable_if 模板编译期检查
conceptC++ http://www.generic-programming.org/faq/?category=conceptcxx Checking Concept Without Conc ...
- day051 Django创建
Django的下载安装 下载Django: pip3 install django==1.11.14 创建Django project(项目) 步骤1: 步骤2: 步骤3: 配置settings属性 ...
- makeObjectsPerformSelector对数组中的对象发送消息执行对象中方法
- (void)makeObjectsPerformSelector:(SEL)aSelector; - (void)makeObjectsPerformSelector:(SEL)aSelector ...
- Spring Boot的事务管理注解@EnableTransactionManagement的使用
@EnableTransactionManagement:负责开启springboot 的事物支持,等同于xml配置文件中的 <tx:annotation-driven /> 然后在访问数 ...
- 自动化测试-6.selenium的css定位
前言 大部分人在使用selenium定位元素时,用的是xpath定位,因为xpath基本能解决定位的需求.css定位往往被忽略掉了,其实css定位也有它的价值,css定位更快,语法更简洁.这一篇css ...
- 2018年3月底的PTA(二)
C高级第二次PTA作业(1) 题目6-7 删除字符串中数字字符 1.设计思路 为了偷懒,本题算法和流程图是精简代码后的,具体请看本题实验代码的第二段代码. (1)算法(子函数) 第一步:定义子函数类型 ...
- scrapy框架之递归解析和post请求
递归爬取解析多页页面数据 scrapy核心组件工作流程 scrapy的post请求发送 1.递归爬取解析多页页面数据 - 需求:将糗事百科所有页码的作者和段子内容数据进行爬取切持久化存储 - 需求分析 ...
- c——二分查找
思路: 1.输入:数组长度n,待查找的有序数组a[],要找的元素key 2.输出:待查找元素在数组中的位置,若不存在返回-1 3.实现:三个指针,left.mid.right #include< ...