总结
大厂面试问深度,小厂面试问广度,如果有同学想进大厂深造一定要有一个方向精通的惊艳到面试官,还要平时遇到问题后思考一下问题的本质,找方法解决是一个方面,看到问题本质是另一个方面。还有大家一定要有目标,我在很久之前就想着以后一定要去大厂,然后默默努力,每天看一些大佬们的文章,总是觉得只有再学深入一点才有机会,所以才有恒心一直学下去。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
console.log(Hello , my name is ${this.name}
);
}
let handsomeBoy = new Person(‘Nealyang’,25);
console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true
handsomeBoy.sayName(); // Hello , my name is Nealyang
从上面的例子我们可以看到:
-
访问到
Person
构造函数里的属性 -
访问到
Person.prototype
中的属性
new 手写版本一
function objectFactory() {
const obj = new Object(),//从Object.prototype上克隆一个对象
Constructor = [].shift.call(arguments);//取得外部传入的构造器
const F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正确的原型
Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性
return obj;//返回 obj
};
-
用
new Object()
的方式新建了一个对象 obj -
取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以
arguments
会被去除第一个参数 -
将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
-
使用
apply
,改变构造函数this
的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性 -
返回 obj
下面我们来测试一下:
function Person(name,age){
this.name = name;
this.age = age;
this.sex = ‘male’;
}
Person.prototype.isHandsome = true;
Person.prototype.sayName = function(){
console.log(Hello , my name is ${this.name}
);
}
function objectFactory() {
let obj = new Object(),//从Object.prototype上克隆一个对象
Constructor = [].shift.call(arguments);//取得外部传入的构造器
console.log({Constructor})
const F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正确的原型
Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性
return obj;//返回 obj
};
let handsomeBoy = objectFactory(Person,‘Nealyang’,25);
console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true
handsomeBoy.sayName(); // Hello , my name is Nealyang
注意上面我们没有直接修改 obj 的__proto__
隐式挂载。
new 手写版本二
考虑构造函数又返回值的情况:
-
如果构造函数返回一个对象,那么我们也返回这个对象
-
如上否则,就返回默认值
function objectFactory() {
var obj = new Object(),//从Object.prototype上克隆一个对象
Constructor = [].shift.call(arguments);//取得外部传入的构造器
var F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正确的原型
var ret = Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性
return typeof ret === ‘object’ ? ret : obj;//确保构造器总是返回一个对象
};
关于 call、apply、bind、this 等用法和原理讲解:【THE LAST TIME】this:call、apply、bind
类式继承
function SuperClass() {
this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
return this.superValue;
}
function SubClass() {
this.subValue = false;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getSubValue = function() {
return this.subValue;
}
var instance = new SubClass();
console.log(instance instanceof SuperClass)//true
console.log(instance instanceof SubClass)//true
console.log(SubClass instanceof SuperClass)//false
从我们之前介绍的
instanceof
的原理我们知道,第三个console
如果这么写就返回true
了console.log(SubClass.prototype instanceof SuperClass)
虽然实现起来清晰简洁,但是这种继承方式有两个缺点:
-
由于子类通过其原型prototype对父类实例化,继承了父类,所以说父类中如果共有属性是引用类型,就会在子类中被所有的实例所共享,因此一个子类的实例更改子类原型从父类构造函数中继承的共有属性就会直接影响到其他的子类
-
由于子类实现的继承是靠其原型prototype对父类进行实例化实现的,因此在创建父类的时候,是无法向父类传递参数的。因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化
构造函数继承
function SuperClass(id) {
this.books = [‘js’,‘css’];
this.id = id;
}
SuperClass.prototype.showBooks = function() {
console.log(this.books);
}
function SubClass(id) {
//继承父类
SuperClass.call(this,id);
}
//创建第一个子类实例
var instance1 = new SubClass(10);
//创建第二个子类实例
var instance2 = new SubClass(11);
instance1.books.push(‘html’);
console.log(instance1)
console.log(instance2)
instance1.showBooks();//TypeError
SuperClass.call(this,id)
当然就是构造函数继承的核心语句了.由于父类中给this绑定属性,因此子类自然也就继承父类的共有属性。由于这种类型的继承没有涉及到原型prototype
,所以父类的原型方法自然不会被子类继承,而如果想被子类继承,就必须放到构造函数中,这样创建出来的每一个实例都会单独的拥有一份而不能共用,这样就违背了代码复用的原则,所以综合上述两种,我们提出了组合式继承方法
组合式继承
function SuperClass(name) {
this.name = name;
this.books = [‘Js’,‘CSS’];
}
SuperClass.prototype.getBooks = function() {
console.log(this.books);
}
function SubClass(name,time) {
SuperClass.call(this,name);
this.time = time;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getTime = function() {
console.log(this.time);
}
如上,我们就解决了之前说到的一些问题,但是是不是从代码看,还是有些不爽呢?至少这个SuperClass
的构造函数执行了两遍就感觉非常的不妥.
原型式继承
function inheritObject(o) {
//声明一个过渡对象
function F() { }
//过渡对象的原型继承父对象
F.prototype = o;
//返回过渡对象的实例,该对象的原型继承了父对象
return new F();
}
原型式继承大致的实现方式如上,是不是想到了我们new
关键字模拟的实现?
其实这种方式和类式继承非常的相似,他只是对类式继承的一个封装,其中的过渡对象就相当于类式继承的子类,只不过在原型继承中作为一个普通的过渡对象存在,目的是为了创建要返回的新的实例对象。
var book = {
name:‘js book’,
likeBook:[‘css Book’,‘html book’]
}
var newBook = inheritObject(book);
newBook.name = ‘ajax book’;
newBook.likeBook.push(‘react book’);
var otherBook = inheritObject(book);
otherBook.name = ‘canvas book’;
otherBook.likeBook.push(‘node book’);
console.log(newBook,otherBook);
如上代码我们可以看出,原型式继承和类式继承一个样子,对于引用类型的变量,还是存在子类实例共享的情况。
所以,我们还有下面的寄生式继
寄生式继承
var book = {
name:‘js book’,
likeBook:[‘html book’,‘css book’]
}
function createBook(obj) {
//通过原型方式创建新的对象
var o = new inheritObject(obj);
// 拓展新对象
o.getName = function(name) {
console.log(name)
}
// 返回拓展后的新对象
return o;
}
其实寄生式继承就是对原型继承的拓展,一个二次封装的过程,这样新创建的对象不仅仅有父类的属性和方法,还新增了别的属性和方法。
寄生组合式继承
回到之前的组合式继承,那时候我们将类式继承和构造函数继承组合使用,但是存在的问题就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了寄生组合式继承
而寄生组合式继承是寄生式继承和构造函数继承的组合。但是这里寄生式继承有些特殊,这里他处理不是对象,而是类的原型。
function inheritObject(o) {
//声明一个过渡对象
function F() { }
//过渡对象的原型继承父对象
F.prototype = o;
//返回过渡对象的实例,该对象的原型继承了父对象
return new F();
}
function inheritPrototype(subClass,superClass) {
// 复制一份父类的原型副本到变量中
var p = inheritObject(superClass.prototype);
// 修正因为重写子类的原型导致子类的constructor属性被修改
p.constructor = subClass;
// 设置子类原型
subClass.prototype = p;
}
组合式继承中,通过构造函数继承的属性和方法都是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。
我们需要继承的仅仅是父类的原型,不用去调用父类的构造函数。换句话说,在构造函数继承中,我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们可以通过原型继承拿到,但是这么直接赋值给子类会有问题,因为对父类原型对象复制得到的复制对象p中的constructor
属性指向的不是subClass
子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复起constructor
属性指向性不正确的问题,最后将得到的复制对象p赋值给子类原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。
function SuperClass(name) {
this.name = name;
this.books=[‘js book’,‘css book’];
}
SuperClass.prototype.getName = function() {
console.log(this.name);
}
function SubClass(name,time) {
SuperClass.call(this,name);
this.time = time;
}
inheritPrototype(SubClass,SuperClass);
SubClass.prototype.getTime = function() {
console.log(this.time);
}
var instance1 = new SubClass(‘React’,‘2017/11/11’)
var instance2 = new SubClass(‘Js’,‘2018/22/33’);
instance1.books.push(‘test book’);
console.log(instance1.books,instance2.books);
instance2.getName();
instance2.getTime();
这种方式继承其实如上图所示,其中最大的改变就是子类原型中的处理,被赋予父类原型中的一个引用,这是一个对象,因此有一点你需要注意,就是子类在想添加原型方法必须通过prototype.来添加,否则直接赋予对象就会覆盖从父类原型继承的对象了.
ES6 类的实现原理
关于 ES6 中的 class 的一些基本用法和介绍,限于篇幅,本文就不做介绍了。该章节,我们主要通过 babel的 REPL来查看分析 es6 中各个语法糖包括继承的一些实现方式。
基础类
我们就会按照这个类,来回摩擦。然后再来分析编译后的代码。
“use strict”;
function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== “undefined” &&
right[Symbol.hasInstance]
) {
return !!rightSymbol.hasInstance;
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError(“Cannot call a class as a function”);
}
}
var Person = function Person(name) {
_classCallCheck(this, Person);
this.name = name;
};
_instanceof
就是来判断实例关系的的。上述代码就比较简单了,_classCallCheck
的作用就是检查 Person
这个类,是否是通过new
关键字调用的。毕竟被编译成 ES5 以后,function
可以直接调用,但是如果直接调用的话,this
就指向 window
对象,就会Throw Error
了.
添加属性
“use strict”;
function _instanceof(left, right) {…}
function _classCallCheck(instance, Constructor) {…}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var Person = function Person(name) {
_classCallCheck(this, Person);
_defineProperty(this, “shili”, ‘实例属性’);
this.name = name;
};
_defineProperty(Person, “jingtai”, ’ 静态属性’);
其实就是讲属性赋值给谁的问题。如果是实例属性,直接赋值到 this
上,如果是静态属性,则赋值类上。_defineProperty
也就是来判断下是否属性名重复而已。
添加方法
“use strict”;
function _instanceof(left, right) {…}
function _classCallCheck(instance, Constructor) {…}
function _defineProperty(obj, key, value) {…}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if (“value” in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var Person =
/#PURE/
function () {
function Person(name) {
_classCallCheck(this, Person);
_defineProperty(this, “shili”, ‘实例属性’);
this.name = name;
}
_createClass(Person, [{
key: “sayName”,
value: function sayName() {
return this.name;
}
}, {
key: “name”,
get: function get() {
return ‘Nealyang’;
},
set: function set(newName) {
console.log(‘new name is :’ + newName);
}
}], [{
key: “eat”,
value: function eat() {
return ‘eat food’;
}
}]);
return Person;
}();
_defineProperty(Person, “jingtai”, ’ 静态属性’);
看起来代码量还不少,其实就是一个_createClass
函数和_defineProperties
函数而已。
首先看_createClass
这个函数的三个参数,第一个是构造函数,第二个是需要添加到原型上的函数数组,第三个是添加到类本身的函数数组。其实这个函数的作用非常的简单。就是加强一下构造函数,所谓的加强构造函数就是给构造函数或者其原型上添加一些函数。
而_defineProperties
就是多个_defineProperty
(感觉是废话,不过的确如此)。默认 enumerable
为 false
,configurable
为 true
。
其实如上就是 es6 class 的实现原理。
extend 关键字
“use strict”;
function _instanceof(left, right) {…}
function _classCallCheck(instance, Constructor) {…}
var Parent = function Parent(name) {…};
function _typeof(obj) {
if (typeof Symbol === “function” && typeof Symbol.iterator === “symbol”) {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj && typeof Symbol === “function” && obj.constructor === Symbol && obj !== Symbol.prototype ? “symbol” : typeof obj;
};
}
return _typeof(obj);
}
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === “object” || typeof call === “function”)) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(“this hasn’t been initialised - super() hasn’t been called”);
}
return self;
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.proto || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _inherits(subClass, superClass) {
if (typeof superClass !== “function” && superClass !== null) {
throw new TypeError(“Super expression must either be null or a function”);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.proto = p;
return o;
};
return _setPrototypeOf(o, p);
}
var Child =
/#PURE/
function (_Parent) {
_inherits(Child, _Parent);
function Child(name, age) {
var _this;
_classCallCheck(this, Child);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name)); // 调用父类的 constructor(name)
_this.age = age;
return _this;
}
return Child;
}(Parent);
var child1 = new Child(‘全栈前端精选’, ‘0.3’);
console.log(child1);
删去类相关的代码生成,剩下的就是继承的语法糖剖析了。其中super
关键字表示父类的构造函数,相当于 ES5 的 Parent.call(this)
,然后再根据我们上文说到的继承方式,有没有感觉该集成的实现跟我们说的寄生组合式继承非常的相似呢?
在 ES6 class 中,子类必须在 constructor
方法中调用 super
方法,否则新建实例时会报错。这是因为子类没有自己的 this
对象,而是继承父类的 this
对象,然后对其进行加工。如果不调用 super
方法,子类就得不到 this
对象。
也正是因为这个原因,在子类的构造函数中,只有调用 super
之后,才可以使用 this
关键字,否则会报错。
关于 ES6 中原型链示意图可以参照如下示意图:
图片来自冴羽的博客
关于ES6 中的 extend
关键字,上述代码我们完全可以根据执行来看。其实重点代码无非就两行:
_inherits(Child, _Parent);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
我们分别来分析下具体的实现:
_inherits
代码比较简单,都是上文提到的内容,就是建立 Child 和 Parent 的原型链关系。代码解释已备注在代码内
function _inherits(subClass, superClass) {
if (typeof superClass !== “function” && superClass !== null) {//subClass 类型判断
throw new TypeError(“Super expression must either be null or a function”);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {//Object.create 第二个参数是给subClass.prototype添加了 constructor 属性
value: subClass,
writable: true,
configurable: true//注意这里enumerable没有指名,默认是 false,也就是说constructor为不可枚举的。
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.proto = p;
return o;
};
return _setPrototypeOf(o, p);
}
_possibleConstructorReturn
_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
根据上图我们整理的 es6 原型图可知:
Child.prototype === Parent
所以上面的代码我们可以翻译为:
_this = _possibleConstructorReturn(this, Parent.call(this, name));
然后我们再一层一层拨源码的实现
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === “object” || typeof call === “function”)) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(“this hasn’t been initialised - super() hasn’t been called”);
}
return self;
}
上述代码,self
其实就是 Child 的 IIFE返回的 function
new
调用的 this
,打印出来结果如下:
这里可能对Parent.call(this,name)
有些疑惑,没关系,我们可以在 Chrome 下调试下。
可以看到,当我们 Parent
的构造函数这么写
class Parent {
constructor(name) {
this.name = name;
}
}
那么最终,传递给_possibleConstructorReturn
函数的第二参数 call
就是一个undefined
。所以在_possibleConstructorReturn
函数里面会对 call
进行判断,返回正确的this
指向:Child
。
所以整体代码的目的就是根据 Parent 构造函数的返回值类型确定子类构造函数 this 的初始值 _this。
最后
–
【THE LAST TIME】系列关于 JavaScript 基础的文章目前更新三篇,我们最后再来一道经典的面试题吧!
function Foo() {
getName = function() {
alert(1);
};
return this;
}
Foo.getName = function() {
alert(2);
};
Foo.prototype.getName = function() {
alert(3);
};
var getName = function() {
alert(4);
};
function getName() {
alert(5);
}
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
老铁,评论区留下你的思考吧~
参考文献
计算机网络
-
HTTP 缓存
-
你知道 302 状态码是什么嘛?你平时浏览网页的过程中遇到过哪些 302 的场景?
-
HTTP 常用的请求方式,区别和用途?
-
HTTPS 是什么?具体流程
-
三次握手和四次挥手
-
你对 TCP 滑动窗口有了解嘛?
-
WebSocket与Ajax的区别
-
了解 WebSocket 嘛?
-
HTTP 如何实现长连接?在什么时候会超时?
-
TCP 如何保证有效传输及拥塞控制原理。
-
TCP 协议怎么保证可靠的,UDP 为什么不可靠?
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
算法
-
链表
-
字符串
-
数组问题
-
二叉树
-
排序算法
-
二分查找
-
动态规划
-
BFS
-
栈
-
DFS
-
回溯算法