系列开篇
为进入前端的你建立清晰、准确、必要的概念和这些概念的之间清晰、准确、必要的关联, 让你不管在什么面试中都能淡定从容。没有目录,而是通过概念关联形成了一张知识网络,往下看你就明白了。当你遇到【关联概念】时,可先从括号中的(强/弱)判断简单这个关联是对你正在理解的概念是强相关(得先理解你才能继续往下)还是弱相关(知识拓展)从而提高你的阅读效率。我也会定期更新相关关联概念。
面试题
- 什么是原型,原型链
- js中是如何实现继承关系的(基于原型链的继承)
[[Prototype]] 、__proto__ 、 prototype属性、constructor
等等都是干啥的- 我们平时能用这些个知识做些什么事
- js 的面向对象思想
这是干什么的?
我们先给个MDN上的定义,帮助我们初步了解这些概念,以及这些概念的关联。
MDN: JavaScript 常被描述为一种基于原型的语言 (
prototype-based language
)——每个对象拥有一个原型对象,对象以其原型为模板
、从原型继承方法和属性
。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain
),它解释了为何一个对象会拥有定义在其他对象中的属性和方法
。
在传统的面向对象编程 (OOP) 中,首先定义“类” (Class),此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__
属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
ok 我们现在先留下一个大致印象。模糊的了解了什么是原型和原型链。
原型继承
在编程中,我们经常会想获取并扩展一些东西,这非常常见,比如你有 people
这个对象及其属性和方法,并希望将 student
和 worker
作为基于 people
稍加修改的变体。我们想重用
people
中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象。原型继承(Prototypal inheritance) 这个语言特性能够帮助我们实现这一需求。
[[Prototype]]
在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]]
,它要么为 null
,要么就是对另一个对象的引用
。该对象被称为“原型”, 也可以理解为这个属性是指向原型对象的指针。
就像这样,当我们从 object 中读取一个缺失的属性时,JavaScript 会自动从原型对象中获取该属性。
__proto__
我们可以用 __proto__
来设置 [[Prototype]]
这个隐藏属性。不过现在有新的访问方式。
__proto__
是 [[Prototype]]
属性由于历史原因而留下来的 getter/setter
。
MDN: 遵循ECMAScript标准,
someObject.[[Prototype]]
符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]]
可以通过Object.getPrototypeOf()
和Object.setPrototypeOf()
访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性__proto__
。
首先明确这不是一个东西。再强调__proto__
是 [[Prototype]]
的 getter/setter
。
但由于 __proto__
标记在观感上更加明显,所以我们在下面的示例中将使用它。
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
// 设置 rabbit.[[Prototype]] = animal
rabbit.__proto__ = animal;
可以说 animal
是 rabbit
的原型
,或者说 rabbit
的原型
是从 animal
继承
而来的。
现在,如果我们从 rabbit 中读取一个它没有的属性,JavaScript 会自动从 animal 中获取。
alert( rabbit.eats ); // true
当 alert 试图读取 rabbit.eats
时,因为它不存在于 rabbit 中,所以 JavaScript 会顺着 [[Prototype]]
引用,在 animal
中查找(自下而上)
因此,如果 animal
中有许多有用的属性和方法,那么它们将自动地变为在 rabbit
中可用。这种属性被称为“继承”。
例如如果我们在 animal 中有一个方法,它也可以在 rabbit 中被调用:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// walk 方法是从原型中获得的
rabbit.walk(); // Animal walk
原型链可以很长,原型还能有原型,但不能有闭环,如果我们试图在一个闭环中分配 __proto__
,JavaScript 会抛出错误。并且只能有一个 [[Prototype]]
。一个对象不能从其他两个对象获得继承。这跟OOP的多重继承 [核心概念] 不同 。
现在我们对原型和原型链有了更进一步的认识。当读取实例的属性时,如果找不到,就会查找与对象关联的原型
中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
F.prototype
我们现在知道,一个对象可能是另一个对象的原型,那么平时我们如何创建对象的?
// 字面量,没毛病
let a = {}
// new 关键字
let b = new Fun(n,m,..)
// 又或者 Object.create 等等
我们来讨论下 new F
这样的用构造函数来创建一个新对象。
如果 F.prototype
是一个对象,那么 new
操作符会使用它为新对象设置 [[Prototype]]
。
也就是说,F.prototype
这个属性指向的是一个对象(假设是A),那么 用 let b = new F()
创建的实例(b)的[[Prototype]]
就指向这个对象(A),也就是作为这个新实例的原型对象(A)
注意,这里的 F.prototype
指的是 F
的一个名为 "prototype" 的常规属性。
看个具体例子
let animal = {
eats: true
};
// 构造函数
function Rabbit(name) {
this.name = name;
}
// 构造函数的 prototype 属性设置为 animal
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
alert( rabbit.eats ); // true
设置 Rabbit.prototype = animal
的字面意思是:“当创建了一个 new Rabbit
时,把它的 [[Prototype]]
赋值为 animal
”。
在上图中,"prototype" 是一个水平箭头,表示一个常规属性,[[Prototype]]
是垂直的,表示 rabbit
继承自 animal
, 或者说 animal
是 rabbit
的原型。
所以结论是:F.prototype
属性仅在 new F
被调用时使用,它为新对象的 [[Prototype]]
赋值。
另外 F.prototype
的值要么是一个对象,要么就是 null:其他值都不起作用。
但我们平时貌似没手动给 new F
加过 prototype
这个属性啊,因为 js 给我们加了个默认选项,也就是 默认的 F.prototype,构造器属性
每个函数
都有 "prototype"
属性,即使我们没有提供它。
默认的 "prototype"
是一个只有属性 constructor
的对象,属性 constructor 指向函数自身。
function Rabbit() {}
/* default prototype
Rabbit.prototype = { constructor: Rabbit };
*/
我们可以检查一下:
function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }
alert( Rabbit.prototype.constructor == Rabbit ); // true
// 而且 constructor 属性可以通过 [[Prototype]] 给所有实例 rabbits 使用:
let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}
alert(rabbit.constructor == Rabbit); // true (from prototype)
所以我们可以使用 constructor
属性来创建一个新对象,该对象使用与现有对象相同的构造器。类似 new rabbit.constructor();
这样。
这里有个点一定要注意:JavaScript 自身并不能确保正确的 "constructor" 函数值。如果我们将整个默认 prototype 替换掉,那么其中就不会有 "constructor" 了。
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false
所以在操作原型对象时,切忌直接替换,我们可以选择添加/删除属性到默认 "prototype"。
function Rabbit() {}
// 不要将 Rabbit.prototype 整个覆盖
// 可以向其中添加内容
Rabbit.prototype.jumps = true
// 默认的 Rabbit.prototype.constructor 被保留了下来
// 或者,也可以手动重新创建 constructor 属性
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
// 这样的 constructor 也是正确的,因为我们手动添加了它
原生的原型
prototype
属性在 JavaScript 自身的核心部分中被广泛地应用。所有的内置构造函数都用到了它。
Object.prototype
假如我们输出一个空对象:
let obj = {};
alert( obj ); // "[object Object]"
生成字符串 "[object Object]"
的代码在哪里?
obj = {}
和 obj = new Object()
是一个意思,其中 Object 就是一个内建的对象构造函数,其自身的 prototype
指向一个带有 toString 和其他方法的一个巨大的对象。
所以, 当 obj.toString()
被调用时,这个方法是从 Object.prototype
中获取的。
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true
// 请注意在 Object.prototype 上方的链中没有更多的 [[Prototype]]
alert(Object.prototype.__proto__); // null
其他内建对象,像 Array、Date、Function 及其他
,都在 prototype
上挂载了方法。我们看个完整点的
基本数据类型的特殊性
我们看个例子
console.log("123".split(''))
// ["1", "2", "3"]
"123"
不是个字符串基本类型吗,并不是对象,为什么能调用 split
方法?
答案是 临时包装器对象
字符串
、数字
和布尔值
虽然是基本类型但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 String、Number 和 Boolean 被创建
。那么这些方法存储在包装器对象的 prototype 中:String.prototype、Number.prototype 和 Boolean.prototype
。它们提供给我们操作字符串、数字和布尔值的方法然后消失。
注意原始数据类型只有 undefined 和 null
没有包装器对象。所以它们没有方法和属性。并且它们也没有相应的原型。
另外内建原型可以被修改或被用新的方法填充。但是不建议更改它们。
原型链图解析
现在理解原型相关概念后,我们来看下这个图,简单分三部分
- 上面绿色箭头相关部分
// Foo 是个构造方法
function Foo() {}
// f1 f2 是 Foo 这个构造函数 创建的对象实例
let f1 = new Foo()
let f2 = new Foo()
// 它们的 [[Prototype]] 或者说 __proto__ 指向 Foo.prototype 这个原型对象
console.log(f1.__proto__ === Foo.prototype) // true
// 默认的 prototype 是一个只有属性constructor 的对象,属性 constructor 指向函数自身。
console.log(Foo === Foo.prototype.constructor)
- 中间蓝色箭头相关部分
// o1 o2 是普通对象创建方式可以这样,实际一个效果
let o1 = new Object()
let o2 = {}
// 其中 Object 就是一个 内建的对象构造函数
// 其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。
// 类似的 他们的原型 就是 Object.prototype
console.log(o1.__proto__ === Object.prototype) // true
console.log(o2.__proto__ === Object.prototype) // true
// Object.prototype 上方的链中没有更多的 [[Prototype]]
console.log(Object.prototype.__proto__ === null) // true
// 原型对象的 constructor 属性还是指向构造函数自身。
console.log(o2.__proto__.constructor === Object) // true
- 下方红色箭头相关部分
// 只要是个函数 他的__proto__都是指向 Function.prototype
// 可以理解为函数是 new Function() 出来的对象实例
console.log(Foo.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // true
相信这张图,你现在看起来会觉得异常清晰。
另外面试中常考的相关手写方案请看这篇 前端面试必刷手写题系列[4],是关于 instanceof
和 new 关键字
的手写实现。
理解原型之后,我们下篇再说Javascript中的继承[核心概念]
继续下去,你总会有收获。 上面这句话给你们,同样也给我自己前进的动力。
我是摩尔,数学专业,做过互联网研发,测试,产品
致力用技术改变别人的生活,用梦想改变自己的生活
关注我,找到自己的互联网思路,踏实地打牢固自己的技术体系
点赞、关注、评论、谢谢
有问题求助可私信 1602111431@qq.com 我会尽可能帮助你,也可以交个朋友
参考
- You don't know Javascript 上卷
- zh.javascript.info/prototype-i…
- developer.mozilla.org/zh-CN/docs/…