前端面试之js相关问题(二)

4,682 阅读8分钟

上一篇我们讲到了,在前端面试的时候常被问到的函数及函数作用域的问题。今天这篇我们讲js的一个比较重要的甚至在编程的世界都很重要的问题 面向对象 。

在JavaScript中一切都是对象吗?

“一切皆对象!” 大家都对此深信不疑。其实不然,这里面带有很多的语言陷阱,还是不要到处给别人吹嘘一切皆对象为好。

数据类型

JavaScript 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据,最新的 ECMAScript 标准定义了 7 种数据类型:

基本类型

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ECMAScript 6 新定义)

对象类型

  • Object

对象类型涵盖了很多引用类型,任何非基本类型的都是对象类型。如Function(函数Function 是一个附带可被调用功能的常规对象),这里就不在赘述。

根据这个分类可以看出“并非一切接对象”。

我们可以从两方面来区别这两种类型:

区别

可变性

基本类型:不可变类型,无法添加属性;即使添加属性,解析器无法再下一步读取它;

var cat = "cat";
cat.color = "black";
cat.color // undefined

对象类型:可变类型,支持添加和删除属性。

比较和传递

基本类型:按值比较,按值传递;
对象类型:按引用比较,按引用传递。

// 基本类型
var cat = "tom";
var dog = "tom";
cat === dog // true
//对象类型
var cat = {name:"tom"};
var dog = {name:"tom"};
cat === dog //false

我们说的通过引用进行对象比较是:两个对象的值是否相同取决于它们是否指向相同的底层对象 __David Flanagan

所以我们改成这样:

var cat = {name:"tom"}
var dog = cat;
b.name = "Haba"

dog === cat // true

如何检测对象类型?或者怎么检测一个数据是数组类型?

检测一个对象的类型,强烈推荐使用 Object.prototype.toString 方法; 因为这是唯一一个可依赖的方式。 我们使用Object.prototype.toString方法:

Object.prototype.toString.call([])    // "[object Array]"
Object.prototype.toString.call({})    // "[object Object]"
Object.prototype.toString.call(2)    // "[object Number]"

为什么不能用typeOf

typeof只有在基本类型的检测上面才好使,在引用类型(Function除外)里面他返回的都是object,另 typeof null === "object".

简谈面向对象

“面向对象编程(OOP)” 是目前主流的编程范式,其核心思想是将真实世界中各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

对象是单个实物的抽象,一本书,一只猫,一个人都可以是对象。从语言的角度对象就是一个容器封装了属性和方法。

典型面向对象的两大概念:类和 实例

  • 类:对象的类型模板
  • 实例:根据类创建的对象

很遗憾JavaScript没有类的概念,但它使用构造函数(constructor)作为对象的模板。

//构造函数
var Pet = function (name, language) {
    this.name = name;
    this.say = function () {
        console.log(language);
    }
}
// new 关键字生成对象 有关new操作符我们在后面会讲到。
var cat = new Pet('tom', 'meow');
cat.name // tom
cat.say() // meow

new创建一个对象都进行了哪些操作?

new用于新建一个对象,例如:

function Pet () {}
var tom = new Pet();

new进行了如下操作:

  • 创建一个空对象,用this 变量引用该对象并继承该函数的原型
  • 属性和方法加入到this的引用对象中
  • 新创建的对象由this所引用,并且最后隐式的返回this
    模拟过程:
    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.

var Pet = function (name) {
    this.name = name;
    return {notInstance:"blabla"}
}
var cat = new Pet('tom');
cat.name // undefined
cat.notInstance // blabla

阐述原型链?js如何实现继承?

上面的讲到的构造函数,实例对象的属性和方法都在构造函数内部实现。这样的 构造函数有一个缺点:

var cat1 = new Pet('tom', 'meow');
var cat2 = new pet('jery', 'meow');

cat1.say === cat2.say // false

生成两只猫 叫声一样,但是猫的say方法是不一样的,就是说每新建一个对象就生成一个新的say方法。所有的say方法都是同样的行为,完全可以共享。
JavaScript的原型(prototype)可以让我们实现共享。

原型链 ?

JavaScrip可以采用构造器(constructor)生成一个新的对象,每个构造器都拥有一个prototype属性,而每个通过此构造器生成的对象都有一个指向该构造器原型(prototype)的内部私有的链接(proto),而这个prototype因为是个对象,它也拥有自己的原型,这么一级一级直到原型为null,这就构成了原型链.

原型链的工作原理:

function getProperty(obj, prop) {
    if (obj.hasOwnProperty(prop)) //首先查找自身属性,如果有则直接返回
        return obj[prop]
    else if (obj.__proto__ !== null)
        return getProperty(obj.__proto__, prop) //如何不是私有属性,就在原型链上一步步向上查找,直到找到,如果找不到就返回undefind
    else
        return undefined
}

如果跟着原型链一层层的寻找,所有对象都可以寻找到最顶层,Object.prototype, 即Object的构造函数的prototype属性,而Object.prototype对象指向的就是没有任何属性和方法的null对象。

Object.getPrototypeOf(Object.prototype)
// null

原型链表明了一个对象查找他的属性的过程:首先在对象本身上面找 -> 没找到再到对象的原型上找 ->还是找不到就到原型的原型上找 —>直到Object.prototype找不到 -> 返回undefined。(在这种查找中找到就立刻返回)。

constructor 属性

prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
由于constructor属性是一种原型对象与构造函数的关联关系,所以修改原型对象的时候,务必要小心。

var 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所指的对象;
__proto__实际上是某个实体对象的属性,而prototype则是属于构造函数的属性。
__proto__只能在学习或调试的环境下使用。

这里抓住两点:

  • 构造函数通过 prototype 属性访问原型对象
  • 实例对象通过 [[prototype]] 内部属性访问原型对象,浏览器实现了 __proto__属性用于实例对象访问原型对象

Object 为构造函数时,是Function的实例对象;Function为构造函数时,Function.prototype 是对象,那么他就是Object的实例对象。

来看一个题目:

var F = function(){};
Object.prototype.a = function(){};
Function.prototype.b = function(){};
var f = new F();
// f 能取到a,b吗?原理是什么?

根据原型链的关系:

f是F的实例对象,其原型链:

f.__proto__ -> [F prototype].__proto__ -> [Object prototype].__proto__ -> null

F是构造函数,是Function的实例,他的原型链:

F.__proto__ -> [Function prototype].__proto__ -> [Object prototype].__proto__ -> null

由此,只有F能够访问到Function的prototype,答案就是:“f只能a,但是F可以访问a,b”

原型继承

原型继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链。

function Animal(){
    this.super = 'animal';
}
function Cat(name) {
    this.name = name;
    this.food = 'fish';
}
Cat.prototype = new Animal(); // Cat 继承了Animal
Cat.prototype.getFood = function () {
    return this.food;
}

上面的方法中constructor指向有点问题:

var cat = new Cat('tom');
cat.name // tom
cat.super // animal
cat.getFood() // fish
//but
cat.constructor === Cat //false
cat.constructor === Animal //true

cat 的constructor 并没有指向Cat而是指向了父类Animal。我们需要对它进行修正:

function Cat(name){
    this.name = name;
    this.food = 'fish';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
Cat.prototype.getFood = function () {
    return this.food;
}

好了上面就实现了一个简单的原型继承。

总结

js的灵活性造就了实现继承的多样性,或者说因为他没有真正的类和继承,我们可以利用很多中方式来模拟它。原型继承是最有js特色的一直实现方式,也是使用最多的方式。

关于面向对象,我想说js “几乎一切皆对象”,因为有原型链的存在我们能实现类似其他语言的继承。

加上前面一篇,两篇文章已经涵盖了js大半部分面试问题了,接下来的文章可能会讲解一下单线程模型和计时器相关。这块是个难点我也是看了好多资料后才搞明白了大概。这次的面试系列主要还是针对于“中高级前端”,也是一个进阶的层次,各位看官不要灰心一切都会有拨云见日的一天。

女排夺冠了,今年好像好多我关注的时间都有了完美的结局,很是为他们高兴,也希望我的未来也是一个完美的结局,晚安。

请关注我的专栏 《前端杂货铺》

参考文章