前言
构造函数、原型、原型链作为ES5的内容,已经是老生常谈的问题了。首先说说为什么要再次拿起这个话题去说呢?这几天有空我会看一些源码,这些源码的底层实现考虑到兼容性还是来源于ES5
,很多方法的封装以及实现(不管是按照模块封装还是统一实现)都是面向对象的思想,而且webpack
以及rollup
打包之后解析出来的代码利用@babel/core
和@babel/preset-env
转化之后也都是ES5的代码,所以有想再次谈起这个话题,回顾回顾旧知识,温故而知新。
构造函数
什么是构造函数?构造函数就是使用关键字new
创建对象时调用的函数。构造函数的属性可分为两种:1.实例上的属性 2.公用属性
//实例上的属性
function Animal(){
this.name=name;
this.age=18;
}
原型
原型是每个构造函数都有的,在JS规定,每一个构造函数都有一个 prototype
属性,指向另一个对象,注意这个prototype就是一个对象,这个对象的所有属性和方法都会被构造函数所拥有。原型的作用是共享方法,一般情况下,我们的公共属性定义在构造函数里面,公共的方法放到原型对象上。
Animal.prototype.address={location:"野外"};
实例
使用关键字new调用构造函数创建实例
代码语言:javascript复制let a1 = new Animal("猴子");
let a2 = new Animal("小鸡");
原型链
在JavaScript中万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链[1]。
举例说明:person → Person → Object ,普通人继承人类,人类继承对象类。
当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则返回null。每个实例都有__proto__ 指向所属类的原型
下面看几个题,思考一下结果(答案在文末):
代码语言:javascript复制console.log(a1.age === a2.age);
console.log(a1.address === a2.address);
console.log(a1.__proto__ === a2.__proto__);
console.log(a1.constructor === Animal);
console.log(Animal.__proto__ === Function.prototype);
console.log(a1.__proto__.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__);
继承
首先定义一个Animal
的父类构造函数和一个子类Tiger
构造函数.
function Animal(name){
this.name = name;
this.eat = "吃肉";
}
Animal.prototype.address={location:"山里"};
function Tiger(name){
this.name = name;
this.age = 10;
}
Tiger.prototype.say = function(){
console.log("说话");
}
代码语言:javascript复制补充知识:call,apply,bind的可以改变this的指向。call和apply会立刻执行,bind调用函数时才会执行。call和bind第一个参数传入的是对象或者null或者不传,后面参数是字符串。apply第一个参数对象或者null或者不传,后面的参数是数组。为什么vue的
methods
方法中的this总是指向Vue实例vm呢? 就是因为使用bind方法把this绑死了
。
function polyfillBind (fn, ctx) {
function boundFn (a) {
var l = arguments.length;
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length;
return boundFn
}
function nativeBind (fn, ctx) {
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind
: polyfillBind;
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
言归正传,在子类Tiger
构造函数中使用call方法改变
this`的指向实现父类实例上的属性继承。
Animal.call(this)
代码语言:javascript复制let tigger = new Tiger();
console.log(tigger.eat); //吃肉
如果要继承父类的公共属性或者方法可以使用下面的方法:Object.setPrototypeOf[2]
代码语言:javascript复制Tiger.prototype.__proto__ = Animal.prototype 等价于下面的方法
Object.setPrototypeOf(Tiger.prototype,Animal.prototype) //es7
谈到继承我们再谈谈ES5中Object.create()
方法,在面试中有可能被问到哦!Object.create()
和直接new Object()
的区别 我们先看一个例子
let obj = Object.create({a:1})
console.log(obj.a) //1
console.log(obj.__proto__.a) //1
代码语言:javascript复制let obj = new Object({a:1})
console.log(obj.a) //1
console.log(obj.__proto__.a) //undefined
显然:Object.create()
是将对象继承到原型链上,可以通过原型链访问,new Object()
不可以在原型链上访问。
思考一个问题:继承父类的公共属性或者方法能不能使用Object.create()
Tiger.prototype= Object.create(Animal.prototype)
问题又来了tigger.constructor
指向了父级Animal
,解决方法如下
Tiger.prototype= Object.create(Animal.prototype,{constructor:{value:Tiger}})
原理如下:
代码语言:javascript复制function create(parentPrototype){
let Fn = function(){};
Fn.prototype = parentPrototype; //当前函数的原型 只有父类的原型
let fn = new Fn();
fn.constructor = Tiger
return fn //当前的实例可以拿到 Animal.prototype
}
Tiger.prototype = create(Animal.prototype)
为什么不直接使用Tiger.prototype = new Animal()
直接把say
方法都覆盖了,不能这么用
总结:
在使用继承的时候可以用call Object.create()
或者call setPrototypeOf
方法
思考答案
代码语言:javascript复制console.log(a1.arr === a2.arr); //true
console.log(a1.address === a2.address);//true
console.log(a1.__proto__ === a2.__proto__);//true
console.log(a1.constructor === Animal);//true
console.log(Animal.__proto__ === Function.prototype);//true
console.log(a1.__proto__.__proto__ === Object.prototype);//true
console.log(Object.prototype.__proto__);//null
参考资料
[1]原型与原型链详解: https://www.jianshu.com/p/ddaa5179cda6
[2]Object.setPrototypeOf: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf