乐趣区

【前端面试】原型和原型链

1. 题目

如何准确判断一个变量是数组
写一个原型链继承的例子
继承实现的其他方式
es6 实现继承的底层原理是什么
描述 new 一个对象的过程
zepto 及其他源码中如何使用原型链

2. 知识点
2.1 构造函数
特点:以大写字母开头
function Foo(name,age){
//var obj = {}
//this = {}
this.name = name;
this.age = age;
this.class = ‘class1’
// return this
}

var f1 = new Foo(‘liming’,19);
扩展
var o = {} 是 var o = new Object() 的语法糖
var a = [] 是 var a = new Array() 的语法糖
function Foo(){} 相当于 var Foo = new Function(){}
2.2 原型规则
五条规则:
1. 所有引用类型(对象,数组,函数)都具有对象特性,即可以自由扩展属性
2. 所有引用类型(对象,数组,函数)都具有一个__proto__(隐式原型)属性,是一个普通对象
3. 所有的函数都具有 prototype(显式原型)属性,也是一个普通对象
4. 所有引用类型(对象,数组,函数)__proto__值指向它构造函数的 prototype
5. 当试图得到一个对象的属性时,如果变量本身没有这个属性,则会去他的__proto__中去找
for (var key in object) {
// 高级浏览器中已经屏蔽了来自原型的属性
// 建议加上判断保证程序的健壮性
if (object.hasOwnProperty(key)) {
console.log(object[key]);
}
}
2.3 原型链
obj.__ proto . proto . proto __ …
Object.prototype === null
instanceof 用于判断引用类型属于哪个构造函数
obj instanceob Foo
实际意义:判断 Foo.prototype 在不在 obj 的原型链上
3. 题目解答
3.1 如何准确判断一个变量是数组
arr instanceof Array
3.2 写一个原型链继承的例子
封装 dom 查询
function Elem(id){
this.elem = document.getElementById(id);
};

Elem.prototype.html = function(val){
var elem = this.elem;
if (val) {
elem.innerHTML = val;
return this;
}else{
return elem.innerHTML;
}
}

Elem.prototype.on = function(type,fun){
var elem = this.elem;
elem.addEventListener(type,fun);
return this;
}

var div1 = new Elem(‘id1’);
div1.html(“test”).on(‘click’,function(){
console.log(‘ 点击 ’);
})
3.3 继承实现的其他方式
3.3.1 原型继承
var obj = {
0:’a’,
1:’b’,
arr:[1]
}

function Foo(arr2){
this.arr2 = [1]
}

Foo.prototype = obj;

var foo1 = new Foo();
var foo2 = new Foo();

foo1.arr.push(2);
foo1.arr2.push(2);

console.log(foo2.arr); //[1,2]
console.log(foo2.arr2); //[1]
优点:实现简单
缺点:
1. 无法向父类构造函数传参
2. 同时 new 两个对象时改变一个对象的原型中的引用类型的属性时,另一个对象的该属性也会修改。因为来自原型对象的引用属性是所有实例共享的。
3.3.2 构造继承
function Super(b){
this.b = b;
this.fun = function(){}
}
function Foo(a,b){
this.a = a;
Super.call(this,b);
}

var foo1 = new Foo(1,2);
console.log(foo1.b);
优点:可以向父类传参,子类不会共享父类的引用属性
缺点:无法实现函数复用,每个子类都有新的 fun,太多了就会影响性能,不能继承父类的原型对象。
3.3.3 组合继承
function Super(){
// 只在此处声明基本属性和引用属性
this.val = 1;
this.arr = [1];
}
// 在此处声明函数
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3…
function Sub(){
Super.call(this); // 核心
// …
}
Sub.prototype = new Super();
优点:不存在引用属性共享问题,可传参,函数可复用
缺点:父类的属性会被实例化两次,获取不到真正实例父类(无法区分实例是父类创建还是父类创建的)
优化:
function Super(b){
this.b = b;
this.fun = function(){}
}

Super.prototype.c = function(){console.log(1111)}

function Foo(a,b){
this.a = a;
Super.call(this,b);
}

Foo.prototype = Super.prototype;
// 修复构造函数:
var foo1 = new Foo(1,2);

缺点:无法区分实例是父类创建还是子类创建的
3.3.4 寄生组合继承
function Super(b){
this.b = b;
}

Super.prototype.c = function(){console.log(1111)}

function Foo(a,b){
this.a = a;
Super.call(this,b);
}

var f = new Function();
f.prototype = Super.prototype;
Foo.prototype = new f();
// 等同于 Foo.prototype = Object.create(Super.prototype);

var foo1 = new Foo(1,2);
对父类的 prototype 进行一次寄生,即包装成一个空对象的 prototype,再把这个对象实例化出来作为子类的 peototype。
缺点:无法区分实例是父类创建还是子类创建的
可以添加以下代码:
Foo.prototype.constructor = Foo
这种解决方法不能用于上面的组合优化方法,因为子类父类引用的是同一个原型对象,修改会同时修改。
总结:
继承主要是实现子类对父类方法,属性的复用。
来自原型对象的引用属性是所有实例共享的,所以我们要避免从原型中继承属性。
在构造函数中通过 call 函数可以继承父类构造函数的属性和方法,但是通过这种方式实例化出来的实例会将父类方法多次存储,影响性能。
通过组合继承我们使用 call 继承属性,使用原型继承方法,可以解决以上两个问题,但是通过这种方式实例化出来的对象会存储两份父类构造函数中的属性。
用父类的原型构造一个新对象作为子类的原型,就解决了多次存储的问题,所以最终的寄生组合继承就是最佳继承方式,它的缺点就是书写起来比较麻烦。
3.3.6 node 源码中的继承实现

function inherits(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};

function Stream(){
//…
}

function OutgoingMessage() {
Stream.call(this);
//…
}

inherits(OutgoingMessage, Stream);

OutgoingMessage.prototype.setTimeout = …

以上是寄生组合继承的一个实例。
1. 在 OutgoingMessage 构造函数中通过 call 继承 Stream 构造中的属性。
2. 调用 inherits 方法继承 Stream 原型中的属性。
3. 扩展 OutgoingMessage 自身原型的函数。
inherits 方法中使用了 Object.create 方法, 该方法的作用是通过指定的原型对象和属性创建一个新的对象。
ctor.prototype=Object.create(superCtor.prototype,{…..});
该方法实际上就做了我们上面寄生组合继承中的工作
var f = new Function();
f.prototype =superCtor.prototype;
return new f();
后面的参数是给原型对象添加属性, 可选属性 (非必填), 即把自身作为新创建对象的构造函数。
value: 表示 constructor 的属性值;
writable: 表示 constructor 的属性值是否可写;[默认为: false]
enumerable: 表示属性 constructor 是否可以被枚举;[默认为: false]
configurable: 表示属性 constructor 是否可以被配置,例如 对 obj.a 做 delete 操作是否允许;[默认为: false]
3.4 es6 继承的实现方式
参考我这篇文章:https://segmentfault.com/a/11…
3.5 描述 new 一个对象的过程

创建一个对象
{}._proto_ = 构造函数.prototype
this 指向这个对象
执行代码即对 this 赋值
返回 this

3.6 zepto 及其他源码中如何使用原型链
var Zepto = (function(){

var $,zepto = {}

// … 省略 N 行代码 …

$ = function(selector, context){
return zepto.init(selector, context)
}

zepto.init = function(selector, context) {
var dom

// 针对参数情况,分别对 dom 赋值

// 最终调用 zepto.Z 返回的数据
return zepto.Z(dom, selector)
}

fnction Z(dom, selector) {
var i, len = dom ? dom.length : 0
for (i = 0; i < len; i++) this[i] = dom[i]
this.length = len
this.selector = selector || ”
}

zepto.Z = function(dom, selector) {
return new Z(dom, selector)
}

$.fn = {
// 里面有若干个工具函数
}

zepto.Z.prototype = Z.prototype = $.fn

// … 省略 N 行代码 …

return $
})()

window.Zepto = Zepto
window.$ === undefined && (window.$ = Zepto)

退出移动版