上一篇我们讲到了,在前端面试的时候常被问到的函数及函数作用域的问题。今天这篇我们将将js的一个比较重要的甚至在编程的世界都很重要的问题 面向对象 。
在JavaScript中一切都是对象吗?
“一切皆对象!” 大家都对此深信不疑。其实不然,这里面带有很多的语言陷阱,不要到处给别人吹嘘一切接对象为好。
数据类型
JavaScript 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据,最新的 ECMAScript 标准定义了 7 种数据类型:
基本类型
- Boolean
- Null
- Undefined
- Number
- String
- Symbol (ECMAScript 6 新定义)
对象类型
- Object
对象类型涵盖了很多引用类型,任何非基本类型的都是对象类型。如Function(函数Function 是一个附带可被调用功能的常规对象),这里就不在赘述。
根据这个分类可以看出“并非一切接对象”。
我们可以从两方面来区别这两种类型:
区别
可变性
基本类型:不可变类型,无法添加属性;即使添加属性,解析器无法再下一步读取它;
1 | var cat = "cat"; |
对象类型:可变类型,支持添加和删除属性。
比较和传递
基本类型:按值比较,按值传递;
对象类型:按引用比较,按引用传递。
1 | // 基本类型 |
我们说的通过引用进行对象比较是:两个对象的值是否相同取决于它们是否指向相同的底层对象 __David Flanagan
所以我们改成这样:
1 |
|
如何检测对象类型?或者怎么检测一个数据是数组类型?
检测一个对象的类型,强烈推荐使用 Object.prototype.toString 方法; 因为这是唯一一个可依赖的方式。 我们使用Object.prototype.toString方法:
1 | Object.prototype.toString.call([]) // "[object Array]" |
为什么不能用typeOf ?
typeof只有在基本类型的检测上面才好使,在引用类型里面他返回的都是object,另 typeof null === "object"
简谈面向对象
“面向对象编程(OOP)” 是目前主流的编程范式,其核心思想是将真实世界中各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
对象是单个实物的抽象,一本书,一只猫,一个人都可以是对象。从语言的角度对象就是一个容器封装了属性和方法。
典型面向对象的两大概念:类 和 实例
- 类:对象的类型模板
- 实例:根据类创建的对象
很遗憾JavaScript没有类的概念,但它使用构造函数(constructor)作为对象的模板。
1 | //构造函数 |
new创建一个对象都进行了哪些操作?
new用于新建一个对象,例如:
1 |
|
new进行了如下操作:
- 创建一个空对象,用this 变量引用该对象并继承该函数的原型
- 属性和方法加入到this的引用对象中
- 新创建的对象由this所引用,并且最后隐式的返回this
模拟过程:1
2
3
4
5
6
7
8
9
function newObj(Fun,arguments) {
var o = {};
if (Fun && typeof Fun === "function") {
o.__proto__ = Fun.prototype;
Fun.apply(o, arguments);
return o;
}
}
这里需要注意的是,构造函数内部有return语句的情况。如果return 后面跟着一个对象,new命令返回return指定的对象;否则不管return语句直接返回this.
1 | var Pet = function (name) { |
阐述原型链?js如何实现继承?
上面的讲到的构造函数,实例对象的属性和方法都在构造函数内部实现。这样的 构造函数有一个缺点:
1 | var cat1 = new Pet('tom', 'meow'); |
生成两只猫 叫声一样,但是猫的say方法是不一样的,就是说每新建一个对象就生成一个新的say方法。所有的say方法都是同样的行为,完全可以共享。
JavaScript的原型(prototype)可以让我们实现共享。
原型链 ?
JavaScrip可以采用构造器(constructor)生成一个新的对象,每个构造器都拥有一个prototype属性,而每个通过此构造器生成的对象都有一个指向该构造器原型(prototype)的内部私有的链接(proto),而这个prototype因为是个对象,它也拥有自己的原型,这么一级一级直到原型为null,这就构成了原型链.
原型链的工作原理:
1 | function getProperty(obj, prop) { |
如果跟着原型链一层层的寻找,所有对象都可以寻找到最顶层,Object.prototype, 即Object的构造函数的prototype属性,而Object.prototype对象指向的就是没有任何属性和方法的null对象。
1 | Object.getPrototypeOf(Object.prototype) |
原型链表明了一个对象查找他的属性的过程:首先在对象本身上面找 -> 没找到再到对象的原型上找 ->还是找不到就到原型的原型上找 —>直到Object.prototype找不到 -> 返回undefined。(在这种查找中找到就立刻返回)。
constructor 属性
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
由于constructor属性是一种原型对象与构造函数的关联关系,所以修改原型对象的时候,务必要小心。1
2
3
4
5
6
7
8
9
10
11
12var Pet = function (name) {
this.name = name;
}
// 尽量避免这么写,因为会把construct属性覆盖掉。
Pet.prototype = {
say: function () {
console.log('meow');
}
}
// 如果我们覆盖了constructor属性要记得将他指回来。
Pet.prototype.constructor = Pet;
proto属性和prototype属性的区别
prototype是function对象中专有的属性。
proto是普通对象的隐式属性,在new的时候,会指向prototype所指的对象;
ptoto实际上是某个实体对象的属性,而prototype则是属于构造函数的属性。
ptoto只能在学习或调试的环境下使用。
这里抓住两点:
- 构造函数通过 prototype 属性访问原型对象
- 实例对象通过 [[prototype]] 内部属性访问原型对象,浏览器实现了
__proto__
属性用于实例对象访问原型对象
Object 为构造函数时,是Function的实例对象;Function为构造函数时,Function.prototype 是对象,那么他就是Object的实例对象。
来看一个题目:
1 | var F = function(){}; |
根据原型链的关系:
f是F的实例对象,其原型链:
1 |
|
F是构造函数,是Function的实例,他的原型链:
1 | F.__proto__ -> [Function prototype].__proto__ -> [Object prototype].__proto__ -> null |
由此,只有F能够访问到Function的prototype,答案就是:“f只能a,但是F可以访问a,b”
原型继承
原型继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链。
1 | function Animal(){ |
上面的方法中constructor指向有点问题:
1 | var cat = new Cat('tom'); |
cat 的constructor 并没有指向Cat而是指向了父类Animal。我们需要对它进行修正:
1 |
|
好了上面就实现了一个简单的原型继承。
js的灵活性早就了实现继承的多样性,或者说因为他没有真正的类和继承,我们可以利用很多中方式来模拟它。原型继承是最有js特色的一直实现方式,也是使用最多的方式。
关于面向对象,我想说js “几乎一切皆对象”,因为有原型链的存在我们能实现类似其他语言的继承。
其实这加上前面一篇,两篇文章已经涵盖了js大半部分面试问题了,接下来的文章可能会讲解一下单线程模型和事件轮询。这块是个难点我也是看了好多资料后才搞明白了大概。这次的面试系列主要还是针对于“中高级前端”,也是一个进阶的层次,各位看官不要灰心一切都会有拨云见日的一天。
女排夺冠了,今年好像好多我关注的时间都有了完美的结局,很是为他们高兴,也希望我的未来也是一个完美的结局,晚安。
请关注我的专栏 《前端杂货铺》