看到这个标题,第一个想到的可能就是prototype吧。没错,prototype原型继承是javascript的继承方式之一,不过,笔者想聊的不仅仅是js的原型继承,也想总结下前端各个地方实现继承的方式,包括css继承、apply和call函数以及深拷贝继承等。
一、CSS的继承
所谓的页面html都是由一个一个的dom组成,而我们也可以看成是一个个的盒子,大盒子怎么装小盒子,各类的盒子怎么放,而css正是决定着这些盒子怎么放怎么摆设。在css中,存在着三大特性:继承性、层叠性以及优先级。在这篇文章中,笔者会对css的继承展开重点讲解,当然了,这三种特性是相互依存的,也会有所提及。
1、浏览器渲染的原理
想要了解css的继承性,咋们先来了解下浏览器是如何渲染页面和加载页面的,来张图吧:
上图中,我们可以看出浏览器渲染其实是分为两条线,前端页面加载了js、html、css文件后,对于浏览器的渲染,现在的页面js都是在html和css加载完后进行加载,对初始化的页面渲染影响一般不会大,我们可以忽略掉,那么就可以分为css和html的parser:
1、html Parser将html文件渲染成dom
2、css parser将css文件生成css rules之后
两者完成后,会将dom和css 匹配原则合并,渲染成tree展示出来,对parser感兴趣的小伙伴可以去深入了解哈,而渲染成的dom是树形结构,即存在着父子节点,自然而然就会有着继承性。
2、什么是css的继承?
css的继承指的就是子节点默认使用父级节点的属性。不同的浏览器对各个标签语是带有默认的属性的,不使用默认属性就必须要覆盖,因此,为了兼容各种浏览器,前端一般都会有一个base.css,对一些有默认属性的样式进行覆盖。在css中,是有作用域,分为局部作用域(有权限访问父级)以及全局作用域(指的是最顶上的body、html),而局部作用域帮助实现了css的继承性。
3、继承的流程
DOM树继承的流程,如下图,下面的儿子来继承父亲的属性,继承是线性的,简单的说,就是最开始有的,最底部也有。
css的继承在chrome的开发工具中继承于哪个元素是可见的,如下图:
上图中,分了两个继承(inherited),可以看到font-family是继承了body的属性,而一些margin-、list-继承了父级元素ul的样式。
4、能继承的属性
在CSS中,并不是所有的样式都是可以继承的,不可继承的属性比较多,小伙伴们只要记住哪些属性属性是可以继承的就行,只有颜色,文字,字体间距行高对齐方式,和列表的样式可以继承,以color-、font-、text-、line- 开头的属性都是可以继承的,下面是笔者搜索以及总结的一些可以继承的属性:
font,font-*,line-*,text-*,color,color-*,
border-collapse, border-spacing,caption-side,
cursor, direction,empty-cells,letter-spacing,
list-style-image, list-style-position,
list-style-type, list-style, visibility,volume,
white-space,word-spacing,text-transform
5、继承的优势
继承的好处就是可以减少c冗杂的ss代码。
6、继承的规则
css中的筛选器是有优先级的,即哪一个优先级高,权重就高,就会使用哪一个选择器,而权重高的选择器会将权重低的样式给覆盖掉,而我们知道权重的高低是根据id数量,类的数量,标签的数量去计算的。而当样式权重为0的时候,即选择器没有起效果或者某一个可继承的属性没有更高权重的选择器时,就采取继承的属性,如下图:
二、javascript的原型
JavaScript继承可以说是发生在对象与对象之间,而原型链则是实现继承的主要方法。MDN中对原型有这么一段话:
根据上面的解释,我们可以得出下图,即为原型:
MDN中还有这么一句话:当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object )都有一个私有属性(称之为 proto)指向它的原型对象(prototype)。该原型对象也有一个自己的原型对象 ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。扯太多容易乱,暂且瞄下这个概念哈。
1、原型中的术语
在原型中,有很多小伙伴对原型方面的理解存在很大的误区,笔者觉得是对原型中的术语理解不彻底或者混淆了概念。想要了解js的原型继承,需要对面向对象知识中的对象、原型、原型链、构造函数等基础知识掌握透彻,因此给小伙伴们一一介绍下原型中各位“大哥”:
(1)对象
其实所谓的对象就是一个包含相关数据和方法的集合(通常由一些变量和函数组成,我们称之为对象里面的属性和方法),在javascript中,所有的事物都是对象:包括基本的数据类型字符串、数值、数组、函数等,而且是允许自定义对象的。
听到这,单身狗们是不是瞬间兴奋了,在哪?还可以自定义的,美滋滋,神器啊,要多new几个
调皮一下很舒服,那有的小伙伴就好奇了,字符串也是对象?搞笑的吧,ok,我们来瞧瞧下面这张图哈:
我们初始化了一个字符串stringA和stringB,对于stringB有length和split方法是因为new了一个String对象实例,这可以理解吧。但是stringA字符串却可以获取到length属性,更神奇的是还可以使用split方法,咋们并没有定义这些东西啊,怎么能说用就用呢,神奇吧?我们来打印个东西就清晰了:
console.log(stringA.__proto__ === String.prototype) // true
其实stringA初始化时,是继承了String对象中的所有属性和方法,简单的说,是使用了javascript的内建对象,因此stringA能使用split方法并且能获取length,这其实就是原型中的一种基础继承,怎么继承的,待会儿一一介绍完了后面的大哥,再把舞台交给他们。
(2)constructor属性
每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数
(3)构造函数
主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。其实也叫构造器,如:
function Cat(name,color){
this.name = name;
this.color = color;
}
var cat1 = new Cat(‘猫哥’, '亮闪闪')
cat1.name //猫哥
前端的构造函数特点:
a、构造函数的首字母必须大写,用来区分于普通函数
b、内部使用的this对象,来指向即将要生成的实例对象
c、使用New来生成实例对象
缺点:
所有的实例对象都可以继承构造器函数中的属性和方法。但是,同一个对象实例之间,无法共享属性
(4)原型对象(prototype)
其他面向对象语言:面向对象的语言有一个标志,即拥有类(class)的概念,抽象实例对象的公共属性与方法,基于类可以创建任意多个实例对象,一般具有封装、继承、多态的特性!
在javascript中,并不存在类的概念,所有实例对象需要共享的属性和方法,都放在一个对象中,那些不需要共享的属性和方法,就放在构造函数中,以此来模拟类,而这个对象指的就是prototype,即原型对象。
注:在前端中,prototype是函数才有的对象
(5)__proto__
__proto__ 属性是一个访问器属性(一个getter函数和一个setter函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。通过构造函数创建的实例会包含一个__proto__属性,这个__proto__属性是一个指针,指向构造函数的 原型对象。因为javascript都是由对象
a、在ES5中,所有构造函数的__proto__都指向Function.prototype
b、在ES6中,构造函数的__proto__指向它的父类构造函数
注:绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。
(6)实例
实例是对象的具体表示,操作可以作用于实例,实例可以有状态地存储操作结果。实例被用来模拟现实世界中存在的、具体的或原型的东西。都是屁话,简单的说可以理解为实际的例子,构造函数通过new创建得到的对象。
2、原型链
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例对象都包含一个指向原型对象的内部指针(__proto__)。如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针(__proto__),另一个原型也包含着一个指向另一个构造函数的指针(constructor)。假如另一个原型又是另一个类型的实例……这就构成了实例与原型的链条即原型链。
简单的说,原型链的形成就是让 一个类型的实例作为另一个类型的原型对象,原型对象中也有可能存在原型,层层剥离,从而形成原型链:这就是为什么一个对象中会拥有定义为其他对象中的属性和方法。
ok,几位大哥已介绍完毕,来瞧瞧下面这波操作:
function Person() {
}
var person = new Person();
虽然只是简单的几行代码,但是却包含了原型里的所有元素,先来看看内部的关系是怎么指向的。
由相互关联的原型组成的链状结构就是原型链,而红色框框中的那一条关系就是原型链,原型链中的指向其实就是继承的本质。由上图也可得出:
console.log(person.__proto__===Person.prototype) //true
console.log(Person.prototype.constructor===Person) //true
console.log(Object.getPrototypeOf(person)===Person.prototype) //true
在原型链继承中即子构造函数.prototype = new 父构造函数();在函数式编程中,有很多继承的方式以及优化继承的方法,有兴趣的小伙伴可以去了解下,深入javascript。
三、其他继承
1、call和apply方法
使用call和apply方法进行继承:
function Animal() {
this.animal = '动物'
}
Animal.prototype.getName = function() {
console.log('我是dog')
}
function Cat() {
Animal.apply(this, arguments)
}
var cat = new Cat()
cat.animal // 动物
cat.getName() // undefined
这种方法可以继承父类构造函数的属性,但是无法继承prototype属性,即父类中共享的方法和属性,其实有点类似于借用下他爹的方法和属性,并不是实际拥有,内部的prototype原型对象并没有。
2、变量以及作用域链
其实在javascript中,当变量和作用域创建时,会创建一个执行环境,而执行环境都是有一个作用域链的,在函数中,访问一个变量的时候,总是从作用域链的顶端开始查找,如果找到就得到结果,如果找到不到就一直查找,直到作用域链的末端。
function sum(num1, num2){
var sum = num1 + num2;
return sum;
}
var sum = sum(3, 4);
上面的函数具体查找方式如下:
作用域链寻找函数对象的方式跟原型链有点类型,因此在这里也提一下。在javascript中,有几个比较特殊的对象也说下吧:
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一
3、深拷贝(递归)
还记得继承是什么吗?就是多个实例对象共享同一个原型的属性和方法。
传统的继承一般是在创建时候,将构造函数中的属性和方法共享化,从而实现继承。而深拷贝在原型创建后,创建一个实例对象后,再将这个实例对象中的方法和属性拷贝到一个新的对象中,。
// 将obj2的成员拷贝到obj1中, 只拷贝实例成员
function deepCopy(obj1, obj2) {
for (var key in obj2) {
// 判断是否是obj2上的实例成员
if (obj2.hasOwnProperty(key)) {
// 判断是否是引用类型的成员变量
if (typeof obj2[key] == 'object') {
obj1[key] = Array.isArray(obj2[key]) ? [] : {};
deepCopy(obj1[key], obj2[key]);
} else {
obj1[key] = obj2[key];
}
}
}
}
本文由cjfpersonal小弟出版,希望大家踊跃吐槽哈