彻底搞懂JavaScript原型
开局先放一张图,接下来我们可以对照这张图来理解下面的内容。
原型链
1. 原型
1.1 引用类型
所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性(除了"null"意外)
1.2 隐式原型
所有的引用类型(数组、对象、函数),都有一个proto(隐式原型) 属性,属性值是一个普通的对象
1.3 显式原型
所有的函数,都有一个prototype(显式原型)属性,属性值也是一个普通的对象。
2. 构造函数
来看下什么是构造函数。在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。来看下面一个例子:
function Person(name, gender, hobby) {
this.name = name;
this.gender = gender;
this.hobby = hobby;
this.age = 6;
}
var p1 = new Person('zs', '男', 'basketball');
var p2 = new Person('ls', '女', 'dancing');
2.1 构造函数执行过程
function Animal(color) {
this.color = color;
}
当一个函数创建好以后,我们并不知道它是不是构造函数,即使像上面的例子一样,函数名为大写,我们也不能确定。只有当一个函数以 new 关键字来调用的时候,我们才能说它是一个构造函数。就像下面这样:
var dog = new Animal("black");
此时,构造函数会有以下几个执行过程:
(1) 当以 new 关键字调用时,会创建一个新的内存空间,标记为 Person 的实例。
创建内存空间
(2) 函数体内部的 this 指向该内存
函数体内部指向该内存
通过以上两步,我们就可以得出这样的结论。
var p2 = new Person('ls', '女', 'dancing'); // 创建一个新的内存 #f2
var p3 = new Person('ww', '女', 'singing'); // 创建一个新的内存 #f3
每当创建一个实例的时候,就会创建一个新的内存空间(#f2, #f3),创建 #f2 的时候,函数体内部的 this 指向 #f2, 创建 #f3 的时候,函数体内部的 this 指向 #f3。
(3) 执行函数体内的代码
通过上面的讲解,你就可以知道,给 this 添加属性,就相当于给实例添加属性。
(4) 默认返回 this 。
由于函数体内部的 this 指向新创建的内存空间,默认返回 this ,就相当于默认返回了该内存空间,也就是上图中的 #f1。此时,#f1的内存空间被变量 p1 所接受。也就是说 p1 这个变量,保存的内存地址就是 #f1,同时被标记为 Person 的实例。
2.2 构造函数的返回值
构造函数执行过程的最后一步是默认返回 this 。言外之意,构造函数的返回值还有其它情况。下面我们就来聊聊关于构造函数返回值的问题。
(1)没有手动添加返回值,默认返回 this 。
function Person1() {
this.name = 'zhangsan';
}
var p1 = new Person1();
按照上面讲的,我们复习一遍。首先,当用 new 关键字调用时,产生一个新的内存空间 #f11,并标记为 Person1 的实例;接着,函数体内部的 this 指向该内存空间 #f11;执行函数体内部的代码;由于函数体内部的 this 指向该内存空间,而该内存空间又被变量 p1 所接收,所以 p1 中就会有一个 name 属性,属性值为 'zhangsan'。
p1: {
name: 'zhangsan'
}
(2) 手动添加一个基本数据类型的返回值,最终还是返回 this。
function Person2() {
this.age = 28;
return 50;
}
var p2 = new Person2();
console.log(p2.age); // 28
p2: {
age: 28
}
如果上面是一个普通函数的调用,那么返回值就是 50。
(3) 手动添加一个复杂数据类型(对象)的返回值,最终返回该对象
function Person3() {
this.height = '180';
return ['a', 'b', 'c'];
}
var p3 = new Person3();
console.log(p3.height); // undefined
console.log(p3.length); // 3
console.log(p3[0]); // 'a'
再来一个例子
function Person4() {
this.gender = '男';
return { gender: '中性' };
}
var p4 = new Person4();
console.log(p4.gender); // '中性'
关于构造函数的返回值,无非就是以上几种情况。
以上构造函数参考: JS进阶(1) —— 人人都能懂的构造函数
2.3 实现一个new
创建一个自定义类
=>创建一个函数(Function类的实例),直接执行就是普通函数,但是“new 执行”它则被称为一个自定义的类
NEW 函数执行
- 形成一个全新的执行上下文EC
- 形成一个AO变量对象
- ARGUMENTS
- 形参赋值
- 初始化作用域链
- [新]默认创建一个对象,而这个对象就是当前类的实例
- [新]声明其THIS指向,让其指向这个新创建的实例
- 代码执行
- [新]不论其是否写RETURN,都会把新创建的实例返回(特殊点)
function func() {
// let obj={}; //=>这个对象就是实例对象
// this -> obj
let x = 100;
this.num = x + 100; //=>相当于给创建的实例对象新增一个num的属性 obj.num=200 (因为具备普通函数执行的一面,所以只有this.xxx=xxx才和创建的实例有关系,此案例中的x只是AO中的私有变量)
// return obj; 用户自己返回内容,如果返回的是一个引用类型值,则会把默认返回的实例给覆盖掉(此时返回的值就不在是类的实例了)
}
let f = new func();
console.log(f); //=>f是func这个类的实例 {num:200}
let f2 = new func();
console.log(f === f2); //=>false 每一次new出来的都是一个新的实例对象(一个新的堆内存)
console.log(f instanceof func); //=>TRUE instanceof用来检测某一个实例是否属于这个类
func(); //=>this:window AO(FUNC):{x=100} ... 普通函数执行
/*
* 内置NEW的实现原理
* @params
* Func:操作的那个类
* ARGS:NEW类的时候传递的实参集合
* @return
* 实例或者自己返回的对象
*/
function _new(Func, ...args) {
//默认创建一个实例对象(而且是属于当前这个类的一个实例)
// let obj = {};
// obj.__proto__ = Func.prototype; //=>IE大部门浏览器中不允许我们直接操作__proto__
let obj = Object.create(Func.prototype);
//也会把类当做普通函数执行
//执行的时候要保证函数中的this指向创建的实例
let result = Func.call(obj, ...args);
//若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
if ((result !== null && typeof result === "object") || (typeof result === "function")) {
return result;
}
return obj;
}
let sanmao = _new(Dog, '三毛');
sanmao.bark(); //=>"wangwang"
sanmao.sayName(); //=>"my name is 三毛"
console.log(sanmao instanceof Dog); //=>true
2.4 instanceof
instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置。
●vara= {}其实是var a = new Object()的语法糖
●var a = []其实是var a = new Array()的语法糖
●function Foo(...}其实是var Foo = new Function(..)
●使用instanceof判断一个函数是否是一个变量的构造函数
instanceof判断一个变量是否是数组
var arr = []
arr instanceof Array // true
typeof arr // object, typeof 是无法判断是否是数组的
2.5
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
Fn.prototype.getX = function () {
console.log(this.x);
};
Fn.prototype.getY = function () {
console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
3. 原型链解析
所有的引用类型(数组、对象、函数),proto属性值指向它的构造函数的"prototype"属性值。
当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的proto(即它的构造函数的prototype)中寻找。
原型链console.log(obj._ proto__ === object. prototype)
var obj = {}; obj.a = 100;
var arr = []; arr.a = 100;
function fn () {}
fn.a = 100;
console. log(obj.__proto__);
console. log(arr.__proto__);
console. log(fn.__proto__);
console. log(fn. prototype)
console. log(obj.__proto__===Object. prototype)
3.1 小测试
function f1() {}
console.log(typeof f1.prototype) // Object
console.log(typeof Function.prototype) // Function
console.log(typeof Object.prototype) // Object
console.log(typeof Function.prototype.prototype) // Object
// 万物皆对象
Object.prototype.name = "zhijia";
1.0.name // zhijia
1..name // zhijia
1.name // 报错 因为不知道 . 属于1还是属于name,所以报错
Function.name // Function function的原型链与object不在一起,所以打出函数名字
3.2 原型链继承
var father = function(color) {
// constructor == Car 构造函数和初始化这个类是一个东西, constructor即函数本身
this.color = color
console.log(111)
}
father.prototype.sail = function() {
// 挂载到原型上new的时候可以少实例化一次,省去重新构建的成本
console.log('这是'+this.color+'色')
}
var son = function(color) {
// father.call(this) // 绑定this,不写color new son的时候传color时会获取不到,可自测
father.call(this,color) // 绑定this
}
// son.prototype = father.prototype // 子类修改时会修改父类,因为这是按引用传递
// new的时候构造函数会执行
// 1. 拿到父类原型链上的方法
// 2. 不能让构造函数执行两次
// 3. 引用的原型链不能按址引用
// 4. 修正子类的constructor
var __pro = Object.create(father.prototype); // 获取副本
__pro.constructor = son; // 与下句交换位置无差别,自测
son.prototype = __pro;
这里我们顺便提一下es6的继承,相比于es5就简单的多了
// es6继承
class People{
constructor(name,age){
this.name = name;
this.age = age;
}
getName(){
return this.name;
}
}
class English extends People{
constructor(name,age,language){
super(name,age)
this.language = language;
}
introduce(){
console.log("hi,I am" + this.getName())
console.log("I speak" + this.language)
}
}
let en = new English('Byron',26,'english')
en.introduce();