作用: 用于实现基于原型的继承,共享方法和属性
例:
let f = function () {
this.a = 1;
this.b = 2;
}
f.prototype.c = 3
let o = new f()
o.__proto__ === f.prototype // true
Object.getPrototypeOf(o) === f.prototype // true
Object.getPrototypeOf(o).constructor === f // true
o.c // 3
// 完整原型链
o.__proto__ == f.prototype // true
f.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
f.__proto__ === Function.prototype // true
Function.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
代码解释:
对象o有个__proto__属性, 指向的是方法f的prototype属性。当我们访问o.c的时候,发现o本身不存在属性c,会以o的__proto__为目标进行查找,如果存在该属性,返回。如果也没有这个属性,会根据当前查找对象的__proto__为目标,依次进行查找,一直到最上层Object.prototype, 如果也没有,返回undefined。
相关名词解释:
- __proto__: 隐式原型,一个指向原型的引用, 由各家浏览器实现,推荐用getPrototypeOf代替。__proto__不推荐使用,但是为了更直观,本文获取对象原型的方法统一由__proto__属性表示。每个对象都有
- prototype: 原型,一个对象,具有构造函数的方法才有。包括constructor, __proto__, 和添加在原型上的属性。
- 原型链: 由隐式原型作为指向后续节点的指针形成的链表,必须是有限长度,也就是不能有环。
- constructor: 原型对象中的一个属性,指向构造函数
原型链关系图:
注意顺序是null -> Object.prototype -> Function.prototype --> [object, function]
疑问
1. 为什么起点是个null
参考文章:
segmentfault.com/q/101000000…
www.ruanyifeng.com/blog/2014/0…
答: 如果Object.prototype是终点,那我们访问Object.prototype.__proto__的结果就是undefined, undefined用于表示声明或者创建了但是还没赋值的变量,这显然与我们语义不符,因为我们肯定不会赋值。而null表示一个"无"的对象,用在这里刚好。
2. 为什么Function.prototype是个function ???
我一直以为prototype是个对象,突然发现Function.prototype是个function,可以执行,返回undefined.
规范说明:
262.ecma-international.org/6.0/#sec-pr…
答: 函数原型对象被指定为函数对象,以确保与 ECMAScript 2015 规范之前创建的 ECMAScript 代码兼容,而且还不是构造函数,意思是Function.prototype.prototype的值是undefined。
这种特殊的定义是不应该的,但为了兼容之前版本,没办法。
还有下面这个问题:
typeof Function.prototype === 'function' // true
Function.prototype instanceof Function // false
Function.prototype是个方法,但它不是通过Function这个构造函数生成的。很奇怪,一个方法居然不是由Function构造出来的!!!
原因:假设为true的话,会造成下面的情况:
Function.prototype instanceof Function // true
// 等同于
Function.prototype.__proto__ === Function.prototype
我们看到会形成环,不符合原型链有限长度的定义。 我们打印Function.prototype看看
Function.prototype.toString() // 'function () { [native code] }'
[native code] :是 native 的代码实现的 built-in 函数,而不是 JavaScript 代码。
3. 如何判断一个function是否可以使用new这个关键词呢?
我之前的理解是除了箭头函数,其它function都是可以new的,明显是错的,现在发现是有构造函数的才可以使用new关键字。
参考文章:
stackoverflow.com/questions/4…
function is_constructor(f) {
try {
Reflect.construct(String, [], f);
} catch (e) {
return false;
}
return true;
}
// true
is_constructor(function(){});
is_constructor(class A {});
is_constructor(Array);
is_constructor(Function);
is_constructor(new Function);
// false
is_constructor();
is_constructor(undefined);
is_constructor(null);
is_constructor(1);
is_constructor(new Number(1));
is_constructor(Array.prototype);
is_constructor(Function.prototype);
is_constructor(() => {})
is_constructor({method() {}}.method)
is_constructor(() => {})
is_constructor(Math)
is_constructor(JSON)
除了Symbol和BigInt,之所以不提供new是因为没必要,就好比我们声明一个字符串会直接let str = 'str', 而不是let str = new String('str')。报错是通过内部判断this指向判断的。es6有new.target可以判断方法是否通过new进行调用
4. 为什么typeof null === 'object'
参考文章:
juejin.cn/post/684490…
typeof原理:不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位存储其类型信息。
- 000: 对象
- 010: 浮点数
- 100:字符串
- 110:布尔
- 1:整数
- null: 二进制表示全为0
- undefined: −2^30
null的后三位刚好和对象的后三位一样。所以将null识别成了object。这是个bug
通过上方的文章发现源码竟然都没有使用JSVAL_IS_NULL这个方法。
5. 如何理解面向对象和万物皆对象?
在查询疑问4的时候发现了一个有趣的问题, 就是万物皆对象这个概念究竟指什么? 它是对的还是错的?js是一门面向对象的语言怎么理解呢?
参考文章: www.zhihu.com/question/23…
java是基于类的面向对象, js是基于原型的面向对象。都是面向对象的一种方式。
面向对象是一种抽象的思维方式,核心的思想只有一个:万物皆对象。可以理解为认知事物的一种方法。
面向对象的三大特征:封装,继承,多态
封装:
- 解释:把对象的属性和操作(或服务)结合为一个独立的整体,隐藏对象的内部实现细节
- js中的封装: 函数 继承:
- 解释:接口继承和实现继承
- js中的继承: 不支持接口继承,由原型链实现继承 多态:
- 解释: 同一个操作作用于不同的对象上,可以产生不同的解释和不同的执行结果
- js中的多态,由于js是一门弱类型语言,天生支持多态,比如运算符号+,左右两边数字不同,结果不同。或者是不同的对象定义同一个方法。
那如何理解基础类型和对象的关系呢? 比如我们看到一个字符串变量
const str = 'str'
我们会自然而然想到,这个str可以使用很多方法,如split, includes等,我们其实是把str当做一个已经封装了很多方法的对象在用的,如果我们了解拆箱,装箱的概念,发现实际引擎也是把它当做字符串对象处理的,只是存储在栈中。
面向对象不等于面向class, 同样面向对象也并不是100%都是对象。
常见题目:
1. typeof Object
typeof Object === 'function' // true
Object instanceof Function // true
Object.__proto__ === Function.prototype // true
常见的如Array, String, Boolean对象都是function
2. __proto__, prototype, constructor的关系
3. 为什么typeof null = 'object' 和 typeof function = 'function'
参考上面的疑问4, typeof null = 'object'是bug导致, typeof Function = 'function'是故意这么判断的。
3. 先有Function还是先有Object
考点: 原型链
顺序是object.prototype -> Function.prototype --> [object, function]
先有的Object.prototype,Object.prototype构造出Function.prototype,然后Function.prototype构造出Object和Function。
很有意思的判断:
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true
console.log(Object instanceof Function) // true
console.log(Object instanceof Object) // true
instanceof 主要的实现原理是右边变量的 prototype 在左边变量的原型链上
4. 手写instanceof
function myInstanceof(left, right) {
if (typeof left !== 'object' || left === null) return false;
let proto = Object.getPrototypeOf(left);
while (true) {
if (proto == null) return false;
if (proto == right.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
}
console.log('myInstanceof', myInstanceof(new Date(), Date))
5. 为什么基础类型可以使用方法?
考点: 基本类型的装箱与拆箱
例:
const str = "str";
const strArr = str.split('');
split应该是object.prototype的方法,为什么str这个字符串类型的变量能使用这个方法?
答: String, Number等方法是对象,但普通类型不是,万物皆对象是错的,但是普通类型和对象有关
上面代码的实际执行:
const str = "str";
// 引擎会帮我们创建一个对象
const strCopy = String("str");
const strArr = strCopy.split('');
// 用完之后干掉这个对象
strCopy = null
这个操作就是我们说的装箱与拆箱,那么为什么要这么干呢?
- 方便,基本类型直接赋值。
- 基本类型放在栈中,省内存,只在使用的时候暂时包装成对象,其余时间还是以基本类型值的形式存在,能够节省的内存。
6. 实现一个new
function myNew(obj, ...rest){
const newObj = Object.create(obj.prototype);
// 或者
// const newObj = {}
// newObj.setPrototypeOf(obj.prototype)
const result = obj.apply(newObj, rest);
// 结果不为空, 返回结果, 否则,返回新创建的对象
return typeof result === 'object' ? result : newObj;
}
function People(name) {
this.name = name || 'person'
this.age = 18
}
const person = myNew(People, '小王')
7. 实现寄生组合式继承
原型链继承: 修改原型链指向
缺点:
- 原型被共享
- 不能给父类构造函数传参
构造继承: call传入当前this
缺点: - 每次创建子类实例都会创建一遍父方法。
- 不能继承父类原型 组合继承: 原型链实现对原型的继承,构造函数实现实例属性的继承。 缺点:
- 调用了2次父类构造函数, 原型中会存在两份相同的属性/方法。
原型式继承: 利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
缺点: - 原型链继承多个实例的引用类型属性指向相同
- 无法传参 寄生继承: 套个壳子传递参数
寄生组合继承:
function SuperType(name){
this.name = name;
this.friends = ["小红", "小赵", "小王"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
// 构造
SuperType.call(this, name);
this.age = age;
}
function inheritPrototype(subType, superType){
// 原型链 原型式 寄生
subType.prototype = Object.create(superType.prototype, {
constructor: { // 修正constructor指向
value: subType,
enumerable: false, // 不可枚举该属性
writable: true, // 可改写该属性
configurable: true // 可用 delete 删除该属性
}
})
}
inheritPrototype(SubType, SuperType)
// 新增子类原型属性
SubType.prototype.sayAge = function(){
alert(this.age);
}
const instance1 = new SubType("小明", 18);
const instance2 = new SubType("小李", 20);
instance1.friends.push("小八");
console.log(instance1.friends, instance2.friends)
7 看代码说出输出
Function.prototype.a = () => {
console.log(1);
}
Object.prototype.b = () => {
console.log(2);
}
function A() {}
const a = new A();
a.a(); // a.a is not a function
a.b(); // 2
A.a(); // 1
A.b(); // 2
解析:
// 1, 2
a.__proto__ === A.prototype
A.prototype.__proto__ === Object.prototype
// 3, 4
A.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
8. 看代码说输出
function Foo() {
getName = function () { console.log (1); };
return this;
}
Foo.getName = function () { console.log (2);};
Foo.prototype.getName = function () { console.log (3);};
var getName = function () { console.log (4);};
function getName() { console.log (5);}
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
解析:
Foo.getName(); // 2
getName(); // 4 考察变量提升,函数比变量提升更高
Foo().getName(); // 1 Foo()中的getName,没用let或var, 作用域为全局
getName(); // 1 // windwos.getName
// 主要是执行的顺序: new Foo() > Foo() > new Foo
new Foo.getName(); 2
new Foo().getName(); // 3 new Foo() -> .getName()
new new Foo().getName();// 3 new (new Foo().getName())
8. Object.cteate,对象字面量, new Object()的区别
对象字面量最简单,原型指向Object.prototype,是new Object()的简写
Object.cteate原型指向传入的对象,可以为null
Object.create
function object(o) {
function F () {} // 先创建一个临时性的构造函数
F.prototype = o // 然后将传入的对象作为这个构造函数的原型
return new F() // 最后返回了这个临时类型的一个新实例
}
参考文章:
juejin.cn/post/684490…
github.com/jawil/blog/…
www.mollypages.org/tutorials/j…