一 目录
不折腾的前端,和咸鱼有什么区别
二 前言
上面是原型链神图,如果你能理解,你基本不用看这篇文章了。
现在走的记得点上赞或者
star~
这次一定啊亲!
如果不能理解那也没关系,下面 jsliang 跟你慢慢唠叨。
2.1 本文知识点概览
- 构造函数
funciton Person() {} - 实例
const person = new Person() - 原型
Person.prototype - 隐藏属性
constructor- 等式 1:
person.constructor === Person - 等式 2:
Person.prototype.constructor === Person
- 等式 1:
-
new-
new的原生例子 - 手写
new:一、判断首参为函数;二、通过Object.create()创建新对象并且绑定原型链;三、通过call或者apply修正this指向和传参;四、通过typeof判断函数对象和普通对象;五、函数对象和普通对象返回构造函数的return值,否则返回新对象 - 通过对手写
new过程的了解来做题
-
- 查找实例对应的对象的原型
person.__proto__ === Person.prototype - 函数对象指向
-
person.__proto__ === Person.prototype -
Person.__proto__ === Function.prototype
-
- 普通对象指向
-
obj.__proto__ === Object.prototype
-
- 原型链
-
foo.__proto__ === Object.prototype -
F.__proto__ === Function.prototype -
F.__proto__.__proto__ === Object.prototype
-
2.2 为什么需要原型及原型链
jsliang 觉得他 2019 年写过的那个例子可以解释地很清楚:
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function() {
console.log(age + "岁的" + name + "在吃饭。");
}
}
let p1 = new Person("jsliang", 24);
let p2 = new Person("jsliang", 24);
console.log(p1.eat === p2.eat); // false
可以看到,对于同一个函数,我们通过 new 生成出来的实例,都会开出新的一块堆区,所以上面代码中 person 1 和 person 2 的吃饭是不同的(返回 false)。
拥有属于自己的东西(例如房子、汽车),这样很好。
但它也有不好的地方,毕竟 JavaScript 总共就那么点地儿(内存),你不停地建房子,到最后是不是没有空地了?(内存不足)
所以,咱要想个法子,建个类似于共享库的对象(例如小区公共健身房),这样就可以在需要的时候,大家都跑去公共健身房锻炼,就不需要开辟新空间了。
而如何开辟好这个空间,就需要用到原型 prototype。
function Person(name) {
this.name = name;
}
// Person 在它的原型上定义了一块空间 eat,同一个小区的都可以访问它
Person.prototype.eat = function() {
console.log("吃饭");
}
let p1 = new Person("jsliang", 24);
let p2 = new Person("梁峻荣", 24);
console.log(p1.eat === p2.eat); // true
看!这样我们就做好了公共小区健身房了。
跟进了,发车啦!一大波知识点正在前面
三 普通对象和函数对象
在 JavaScript 中,万物皆对象,你要一个吗?new Object() 啊!
当然,就好比同样为人,也区分普通人和天才。
对象也是有分类的,分为 普通对象 和 函数对象。
而 Object 和 Function 都是 JavaScript 自带的函数对象。
下面我们做个小区分:
function fun1() {};
const fun2 = function() {};
const fun3 = new Function();
const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof fun1); // function
console.log(typeof fun2); // function
console.log(typeof fun3); // function
console.log(typeof obj1); // object
console.log(typeof obj2); // object
console.log(typeof obj3); // object
在上面代码中,fun1、fun2、fun3 都是函数对象,obj1、obj2、obj3 都是普通对象。
记住这点,下面我们会很常用~
四 构造函数
- 什么是构造函数?
当任意一个普通函数用于创建一个类对象时,它就被称作构造函数,或构造器。
它有几点特性:
- 默认函数首字母大写
- 通过
new调用一个函数 - 构造函数返回的是一个对象
例如:
function Person(name) {
this.name = name;
}
const person = new Person('jsliang');
这里的 Person 就是构造函数,而 person 则是构造函数 Person 的 实例对象(后面简称实例)。
要清楚构造函数具体内容,我们应该看一下 new 的实现机制,但是现在知识点前置不足,我们后面章节再进行讲解。
小结本章知识点:
- 构造函数
funciton Person() {} - 实例
const person = new Person()
五 原型
在 JavaScript 中,每当定义一个对象的时候,对象中都会包含一些预定义的属性。
其中每个 函数对象 都有一个 prototype 属性,这个属性的指向被称为这个函数对象的 原型对象(简称原型)。
function Person() {};
Person.prototype.name = 'jsliang';
Person.prototype.sayName = function() {
console.log(this.name);
};
const person1 = new Person();
person1.sayName(); // jsliang
const person2 = new Person();
person2.sayName(); // jsliang
// 这两个实例对应的原型方法 sayName 都是一样的
console.log(person1.sayName === person2.sayName); // true
出来了出来了,呼应前言内容,我们通过 prototype 定义了构造函数 Person 的小区公共健身房,这样区里邻居(实例 person1 和 person2)就都可以过去健身了。
小结本章知识点:
- 原型
Person.prototype
六 constructor
讲完原型,我们看看 constructor 是什么:
function Person(name) {
this.name = name;
}
const person = new Person('jsliang');
在上面代码中,有构造函数 Person 和它的实例 person。
在 JavaScript 中,每个实例都会有个隐藏属性 constructor。
而构造函数和实例存在一个等式:
person.constructor === Person; // true
所以得出一个结论:
- 实例的属性
constructor指向构造函数
同时,这个函数的原型的 constructor 会指向这个函数:
Person.prototype.constructor === Person; // true
constructor暂时没找到合适的题目,所以记住上面 2 个点即可~
小结本章知识点:
- 隐藏属性
constructor- 等式 1:
person.constructor === Person - 等式 2:
Person.prototype.constructor === Person
- 等式 1:
七 new
在上面我们了解了 4 个知识点了;
- 构造函数
funciton Person() {} - 实例
const person = new Person() - 原型
Person.prototype - 隐藏属性
constructor- 等式 1:
person.constructor === Person - 等式 2:
Person.prototype.constructor === Person
- 等式 1:
那么,new 在这里起了啥作用?我们先看看原生 new 的一个返回情况:
function Person( name, age){
this.name = name;
this.age = age;
// return; // 返回 this
// return null; // 返回 this
// return this; // 返回 this
// return false; // 返回 this
// return 'hello world'; // 返回 this
// return 2; // 返回 this
// return []; // 返回 新建的 [], person.name = undefined
// return function(){}; // 返回 新建的 function,抛弃 this, person.name = undefined
// return new Boolean(false); // 返回 新建的 boolean,抛弃 this, person.name = undefined
// return new String('hello world'); // 返回 新建的 string,抛弃 this, person.name = undefined
// return new Number(32); // 返回 新的 number,抛弃 this, person.name = undefined
}
var person = new Person("jsliang", 25);
console.log(person); // Person {name: "jsliang", age: 25}
那么下面根据返回情况,我们试试手写 new 吧!
7.1 手写 new
先看一遍最终版的简版:
function myNew(func, ...args) {
if (typeof func !== 'function') {
throw '第一个参数必须是方法体';
}
const obj = {};
obj.__proto__ = Object.create(func.prototype);
let result = func.apply(obj, args);
const isObject = typeof result === 'object' && typeof result !== null;
const isFunction = typeof result === 'function';
return isObject || isFunction ? result : obj;
}
它其中的逻辑道道是:
- 第一个参数必须是个函数。
const person = new Person(),但是我们搞不了原汁原味的,那就变成const person = myNew(Person)。 - 原型链继承。我们新建一个对象
obj,这个obj的__proto__指向func的原型prototype,即obj.__proto__ === func.prototype。 - 修正
this指向。通过apply绑定obj和func的关系,并且将参数作为一个数组传递进去(方法体定义已经将剩余参数解构为数组) - 判断构造函数是否返回
Object或者Function。typeof判断object需要排除null,因为typeof null === object。 - 非函数和对象返回新创建的对象,否则返回构造函数的
return值。
最后贴上详尽注释版本:
function myNew(func, ...args) {
// 1. 判断方法体
if (typeof func !== 'function') {
throw '第一个参数必须是方法体';
}
// 2. 创建新对象
const obj = {};
// 3. 这个对象的 __proto__ 指向 func 这个类的原型对象
// 即实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性
obj.__proto__ = Object.create(func.prototype);
// 为了兼容 IE 可以让步骤 2 和 步骤 3 合并
// const obj = Object.create(func.prototype);
// 4. 通过 apply 绑定 this 执行并且获取运行后的结果
let result = func.apply(obj, args);
// 5. 如果构造函数返回的结果是引用数据类型,则返回运行后的结果
// 否则返回新创建的 obj
const isObject = typeof result === 'object' && typeof result !== null;
const isFunction = typeof result === 'function';
return isObject || isFunction ? result : obj;
}
// 测试
function Person(name) {
this.name = name;
return function() { // 用来测试第 5 点
console.log('返回引用数据类型');
};
}
// 用来测试第 2 点和第 3 点
Person.prototype.sayName = function() {
console.log(`My name is ${this.name}`);
}
const me = myNew(Person, 'jsliang'); // 用来测试第 4 点
me.sayName(); // My name is jsliang
console.log(me); // Person {name: 'jsliang'}
// 用来测试第 1 点
// const you = myNew({ name: 'jsliang' }, 'jsliang'); // 报错:第一个参数必须是方法体
7.2 题目:求输出结果
如果你知道怎么手写一个 new 了,那么下面问题就轻而易举了:
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3,
};
var c = new A();
console.log(b.n); // 输出啥?
console.log(b.m); // 输出啥?
console.log(c.n); // 输出啥?
console.log(c.m); // 输出啥?
求输出结果?
答案:
b.n -> 1
b.m -> undefined
c.n -> 2
c.m -> 3
分析:
b.n和b.m考查的是new的一个实现机制。我们知道这种场景下new操作会生成一个新对象进行挂载,所以在var b = new A()的时候,A的原型(prototype)上挂载的参数就传递过去了。即b.n为1,而b.m为undefined。- 同 1,
c = new A()是在A.prototype改变后定义的,所以这时候A的原型上的数据挂载上去了。
小结本章知识点:
-
new-
new的原生例子 - 手写
new - 通过对手写
new过程的了解来做题
-
八 proto
在上面,我们讲过 Person 有个属于自己的公共区间:Person.prototype。
那么,小区居民如果要去这个公共健身房,这条路怎么走呢?(person 怎么找到 Person.prototype)
在 JavaScript 中,每个 JavaScript 对象(普通对象和函数对象)都具有一个属性 __proto__,这个属性会指向该对象的原型。
看代码:
function Person() {};
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
所以,现在我们小区居民(实例对象)知道怎么找到公共健身房(原型)啦!
小结本章知识点:
- 查找实例对应的对象的原型
person.__proto__ === Person.prototype
九 Object 和 Function 的原型指向
9.1 回顾前面内容
在上面我们将这些基础知识点都了解了,是时候回顾拓展下之前的内容了:
function fun1() {};
const fun2 = function() {};
const fun3 = new Function();
const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof fun1); // function
console.log(typeof fun2); // function
console.log(typeof fun3); // function
console.log(typeof obj1); // object
console.log(typeof obj2); // object
console.log(typeof obj3); // object
jsliang:这份代码中,哪些是函数对象,哪些是普通对象,小伙伴们还记得不?
是的,fun 开头的都是函数对象:fun1、fun2、fun3,特例 Object 和 Function 也是。
下文我们要对 Object 和 Function 动刀了!
9.2 Object 和 Function
function Person() {};
const person = new Person();
在上面代码中,我们看到实例 person 是 new 构造函数 Person 出来的。
那么,Person 又是怎么来的呢?
定义一个函数对象的 3 种方法
const Person = new Function();
// 相当于 function Person() {};
// 或者相当于 const Person = function() {};
console.log(Person.__proto__ === Function.prototype); // true
所以将这两个结合起来:
const Person = new Function();
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.__proto__ === Function.prototype); // true
哦豁,有没有一种豁然开朗的感觉?
jsliang:
Person.prototype.__proto__对应的是什么?下一章揭晓
那么,如果是对象呢?
const obj = new Object();
// 相当于 obj = {};
console.log(obj.__proto__ === Object.prototype); // true
现在我们就搞懂了普通对象和函数对象以及 JavaScript 内置 Object 和 Function 的一个指向情况,下面我们做一道小题目。
9.3 题目:阐述题
function Person(name) {
this.name = name;
}
let p = new Person('jsliang');
问题 1:p.__proto__ 等于什么?
问题 2:Person.__proto__ 等于什么?
答:
p.__proto__ === Person.prototypePerson.__proto__ === Function.prototype
解析:无。本章内容已经讲解了,不需要这里在累述。
小结本章知识点:
- 函数对象指向
-
person.__proto__ === Person.prototype -
Person.__proto__ === Function.prototype
-
- 普通对象指向
-
obj.__proto__ === Object.prototype
-
十 原型链
在上文中,jsliang 询问 Person.prototype.__proto__ 对应的是什么?
看完这章你就能找到答案!
10.1 万物皆对象
首先我们需要从 “万物皆对象” 这句话看起:
function Person() {};
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.__proto__ === Function.prototype); // true
console.log(person.__proto__.__proto__ === Object.prototype); // true
我们先科普一个知识点:
- 通过
__proto__最终查找到的是null
哎,不是万物皆对象吗?
是!但是对象从哪来的,无中生有啊:
Object.prototype.__proto__ === null
对象的原型 prototype 通过 __proto__ 找到最终归宿:null。
这就好比小区公共健身房哪里的,通过空地建的,空地 = null。
这仅仅代表 jsliang 的观点,不同大佬有不同的观点,甚至会从 C++ 等角度解释
OK,那 person.__proto__.__proto__ === Object.prototype 怎么说?
我们看例子:
function Person() {};
Person.prototype.name = 'jsliang2';
Object.prototype.name = 'jsliang3';
const person = new Person();
// 当前展示
console.log(person.name); // jsliang2
// 往实例添加属性
person.name = 'jsliang1';
console.log(person.name); // jsliang1【person 存在该属性】
// 删除实例属性
delete person.name;
console.log(person.name); // jsliang2【Person.prototype 存在该属性】
// 删除构造函数原型
delete Person.prototype.name;
console.log(person.name); // jsliang3 【Object.prototype 存在该属性】
// 删除 Object 原型
delete Object.prototype.name;
console.log(person.name); // undefined 【都删除了,都不存在了】
console.log(person.__proto__ === Person.prototype); // true
console.log(person.__proto__.__proto__ === Object.prototype); // true
原型链:如果一个实例对象不存在某个属性,那么 JavaScript 就会往该构造函数的原型上找;如果该构造函数的原型没找到,那么会继续往 Object 的原型上找;如果 Object 的原型还没有,那就返回 undefined。
这条链:
- 实例对象.xxx -> 构造函数.prototype.xxx -> Object.prototype.xxx
这样是不是就缕清了:
person.__proto__ === Person.prototypePerson.__proto__ === Function.prototypeperson.__proto__.__proto__ === Object.prototypeObject.prototype.__proto__ === null
那么如果有代码:
let str1 = 'jsliang1';
let str2 = new String('jsliang2');
下面这几个对应啥?
str1.__proto__ === ?
str1.__proto__.__proto__ === ?
str2.__proto__ === ?
str2.__proto__.__proto__ === ?
答案:
str1.__proto__和str2.__proto__都等于String.prototypestr1.__proto__.__proto__和str2.__proto__.__proto__都等于Object.prototype
最后再看看其他类型的,相信小伙伴就彻底搞懂 JavaScript 原型链了;
// 字符串
let str = 'jsliang';
console.log(str.__proto__ === String.prototype); // true
console.log(str.__proto__.__proto__ === Object.prototype); // true
// 数字
let num = 25;
console.log(num.__proto__ === Number.prototype); // true
console.log(num.__proto__.__proto__ === Object.prototype); // true
// 布尔值
let bool = false;
console.log(bool.__proto__ === Boolean.prototype); // true
console.log(bool.__proto__.__proto__ === Object.prototype); // true
// 数组
let arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
// 正则
let reg = /jsliang/g;
console.log(reg.__proto__ === RegExp.prototype); // true
console.log(reg.__proto__.__proto__ === Object.prototype); // true
// 日期
let date = new Date();
console.log(date.__proto__ === Date.prototype); // true
console.log(date.__proto__.__proto__ === Object.prototype); // true
万物皆对象~
下面做两个小测试。
10.2 题目 1:求输出结果
var F = function() {};
Object.prototype.a = function() {
console.log('a');
};
Function.prototype.b = function() {
console.log('b');
}
var f = new F();
f.a(); // 输出啥?
f.b(); // 输出啥?
F.a(); // 输出啥?
F.b(); // 输出啥?
求输出结果?
答案:
f.a() -> a
f.b() -> f.b is not a function
F.a() -> a
F.b() -> b
解析:
首先,看 f:
var F = function() {};
var f = new F();
我们知道:
f.__proto__ === F.prototypeF.__proto__ === Function.prototypef.__proto__.__proto__ === Object.prototype
所以,f.a() 会先查找 f 自身存在 a 属性否;如果不存在,往 F 原型 F.prototype 上,看看有没有 a 否;如果不存在,往 Object.prototype 上找,发现 a。
而 f.b 就没有了,它可以通过 F.__proto__ 去查找到,因为定义到 Function.prototype 上了。
10.3 题目 2:求输出结果
var foo = {};
var F = function() {};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';
console.log(foo.a); // 输出啥
console.log(foo.b); // 输出啥
console.log(F.a); // 输出啥
console.log(F.b); // 输出啥
求输出结果?
答:
foo.a -> value a
foo.b -> undefined
F.a -> value a
F.b -> value b
列举关系:
foo.__proto__ === Object.prototypeF.__proto__ === Function.prototypeF.__proto__.__proto__ === Object.prototype
所以:
foo自身没有a属性,那就往Object上去找,从而得到value a。而foo.b就找不到了,返回undefined。F自身没有a属性,那就往Function上去找,没找到;继续往Object.prototype上找,返回value a。而Function.prototype存在b属性,返回value b。
十一 题目
JavaScript 原型和原型链的题目挺多的,后面逐步收录到这里吧!
11.1 选择题
var F = function() {};
Object.prototype.a = function() {};
Function.prototype.b = function() {};
var f = new F();
请选择:
- A:
f能取到a,但取不到b。 - B:
f能取到a、b。 - C:
F能取到b,不能取到a。 - D:
F能取到a,不能取到b。
答案:A
解析:
F.a = functionF.b = functionf.a = functionf.b = undefined
首先,函数 F 通过原型链,可以找到
Object.prototype.a = function() {};
Function.prototype.b = function() {};
这两个绑定的 a 和 b。
但是 f = new F() 出来的是一个 Object,而不是 Function。
所以 f 能找到 a,但是找不到 b。
11.2 阐释原因
Function.prototype.a = 'a';
Object.prototype.b = 'b';
function Person();
var p = new Person();
console.log(p.a); // undefined
console.log(p.b); // b
请讲述下原因?
答案:
Person 函数是 Function 对象的一个实例,所以可以访问 Function 和 Object 原型链上的内容。
而 new Person 返回的是一个对象,只能访问挂载到 Object 原型链上的内容。
所以只有 p.b。
十二 参考文献
如果小伙伴们觉得看完 jsliang 的文章,对原型和原型链还不理解。
或者想更加深入理解,有自己的一套思路,可以看下面的参考文献:
- jsliang 2019 面试 - JavaScript-原型与原型链【阅读建议:1h】
- 【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)【阅读建议:2h】
- 深入理解 JavaScript 原型【阅读建议:1h】
- 【THE LAST TIME】一文吃透所有JS原型相关知识点【阅读建议:30min】
- JavaScript深入之从原型到原型链【阅读建议:30min】
- JavaScript深入之创建对象的多种方式以及优缺点【阅读建议:10min】
- JavaScript 引擎基础:原型优化【阅读建议:10min】
- 详解JS原型链与继承【阅读建议:30min】
- 从proto和prototype来深入理解JS对象和原型链【阅读建议:10min】
- 代码复用模式【阅读建议:10min】
- 深度解析原型中的各个难点【阅读建议:10min】
- 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(一)【阅读建议:内容有些误导】
- 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(二)【阅读建议:高程书摘取,经第一篇后不继续往后看】
- 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(三)【阅读建议:高程书摘取,经第一篇后不继续往后看】
jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。
基于 github.com/LiangJunron… 上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。