笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。
第一章: 谈谈你对原型链的理解
原型对象和构造函数有何关系?
在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象
当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
// 实例.__proto__ === 原型
// 原型.constructor === 构造函数
// 构造函数.prototype === 原型
// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线。例如:
const o = new Object()
console.log(o) //{}
// 注意: 其实实例上并不是真正有 constructor 这个指针,它其实是从原型链上获取的
console.log(o.hasOwnProperty('constructor')) //false
console.log(o.constructor) //[Function: Object]
console.log(o.constructor === Object) // --> true
console.log(o.__proto__.constructor === Object) // --> true
console.log(o.__proto__.constructor.prototype.constructor === Object) // --> true
o.__proto__ = null
console.log(o) //[Object: null prototype] {}
console.log(o.constructor === Object) // --> false
能不能描述一下原型链?
JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链
实例对象.__proto__ = 构造函数的.prototype
class A{}
class B extends A{}
class C extends B{}
console.log(new C())
- 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
- 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
var obj = {name:'一缕清风'}
'toString' in obj // true
obj.hasOwnProperty('toString') // false
⚠️**注意: **的是,虽然in能检测到原型链的属性,但 for in 通常却不行。
第二章: JS如何实现继承?
第一种: 借助call,实现构造函数继承
function Parent() {
this.name = 'parent'
}
Parent.prototype.say = function () {
console.log("Hello Word")
}
function Child() {
Parent.call(this) //继承Parent的属性值
this.type = 'child'
}
console.log(new Child) //Child { name: 'parent', type: 'child' }
console.log(new Parent) //Parent { name: 'parent' }
这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法
第二种: 借助原型链
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child'
}
Child.prototype = new Parent()
console.log(new Child()) //Parent { type: 'child' }
看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play); //[ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]
明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。
那么还有更好的方式么?
第三种:将前两种组合
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Child() {
Parent.call(this) //➕
this.type = 'child'
}
Child.prototype = new Parent()
var s3 = new Child()
var s4 = new Child()
s3.play.push(4)
console.log(s3.play, s4.play) //[ 1, 2, 3, 4 ] [ 1, 2, 3 ]
之前的问题都得以解决。但是这里又徒增了一个新问题
那就是 Parent3 的构造函数会多执行了一次 Child3.prototype = new Parent3()
这是我们不愿看到的。那么如何解决这个问题?
第四种: 组合继承的优化1
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Child() {
Parent.call(this)
this.type = 'child'
}
Child.prototype = Parent.prototype //➕
var s3 = new Child()
var s4 = new Child()
console.log(s3) //Parent { name: 'parent', play: [ 1, 2, 3 ], type: 'child' }
这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试的时候发现子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4
第五种:(最推荐使用): 寄生组合继承的优化2
这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
Parent.prototype.say = function () {
console.log("Hello Word")
}
function Child() {
Parent.call(this)
this.name = 'child'
}
//- Child.prototype = new Parent() // 每次都需要new,带来开销,不推荐。其实还可以写成 Child.prototype = new Parent
Child.prototype = Parent.prototype
Child.prototype.constructor = Child
var child = new Child()
// child.__proto__ = Parent.prototype;
// Object.setPrototypeOf(child, Parent.prototype)
// child.__proto__.constructor = Child
child.say() //Hello Word
console.log(child, child.__proto__) // Child { name: 'child', play: [ 1, 2, 3 ] } Child { say: [Function] }
console.log(child.constructor, child.__proto__.constructor) // [Function: Child] [Function: Child]
console.log(Object.getPrototypeOf(child)) // Child { say: [Function] }
console.log(Object.getPrototypeOf(child.__proto__)) //{}
寄生式组合继承写法上和组合继承基本类似,区别是如下这里:
- Dog.prototype = new Parent()
- Child.prototype.constructor = Child
+ function F() {}
+ F.prototype = Parent.prototype
+ let f = new F()
+ f.constructor = Child
+ Child.prototype = f
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
Parent.prototype.say = function () {
console.log("Hello Word")
}
function Child() {
Parent.call(this)
this.name = 'child'
}
// -Child.prototype = Parent.prototype //直接这样写原型链就不完整
// -Child.prototype.constructor = Child
function F() { }
F.prototype = Parent.prototype
let f = new F()
f.constructor = Child
Child.prototype = f
var child = new Child()
child.say() //Hello Word
console.log(child, child.__proto__) // Child {name: "child", play: Array(3)} Parent {constructor: ƒ}
console.log(child.constructor, child.__proto__.constructor) // [Function: Child] [Function: Child]
console.log(Object.getPrototypeOf(child)) //Child { constructor: [Function: Child] }
console.log(Object.getPrototypeOf(child.__proto__)) // Parent { say: [Function] }
function F() { }
F.prototype = Parent.prototype
let f = new F()
f.constructor = Child
Child.prototype = f
//-------------------- 对上面👆代码稍微封装后的代码 --------------------
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(child, parent) {
let prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
inheritPrototype(Child, Parent)
//------------------------------------------------------------------
var inherit = (function (c, p) {
var F = function () { }
return function (c, p) {
F.prototype = p.prototype
c.prototype = new F()
c.uber = p.prototype
c.prototype.constructor = c
}
})()
//------------------------------------------------------------------
//如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承:
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
第六种:借助 Object.create + Object.getPrototypeOf
function Button() {
this.color = 'red';
}
var button = new Button();
Object.defineProperty(button, 'colorGet', {
// enumerable默认为false。设为可枚举,不然 Object.create | Object.assign 方法会过滤该属性
enumerable: true,
get() {
return "Could it return " + this.color
}
});
var circleButton = Object.create(Object.getPrototypeOf(button), Object.getOwnPropertyDescriptors(button));
console.log(circleButton)
ES6的extends被编译后的JavaScript代码
ES6的代码最后都是要在浏览器上能够跑起来的,这中间就利用了babel这个编译工具,将ES6的代码编译成ES5让一些不支持新语法的浏览器也能运行。
那最后编译成了什么样子呢?
function _possibleConstructorReturn(self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits(subClass, superClass) {
// ...
//看到没有
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Parent = function Parent() {
// 验证是否是 Parent 构造出来的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child() {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent));
核心是_inherits函数,可以看到它采用的依然也是第五种方式 —— 寄生组合继承方式,同时证明了这种方式的成功。不过这里加了一个Object.setPrototypeOf(subClass, superClass),这是用来干啥的呢?
答案是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。
追问: 面向对象的设计一定是好的设计吗?
不一定。从继承的角度说,这一设计是存在巨大隐患的
第七种:class 实现继承
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
从设计思想上谈谈继承本身的问题
假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。
class Car {
constructor(id) {
this.id = id;
}
drive() {
console.log("drive!");
}
music() {
console.log("music!")
}
addOil() {
console.log("addOil")
}
}
class OtherCar extends Car { };
const otherCar = new OtherCar();
otherCar.addOil()
现在可以实现车的功能,并且以此去扩展不同的车。
但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。
如果让新能源汽车的类继承Car的话,也是有问题的,俗称"大猩猩和香蕉"的问题。大猩猩手里有香蕉,但是我现在明明只需要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了
继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承
当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好
那如何来解决继承的诸多问题呢?
用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。
顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。
function drive() {
console.log("drive!")
}
function music() {
console.log("music!")
}
function addOil() {
console.log("addOil")
}
let car = compose(drive, music, addOil)
let newEnergyCar = compose(drive, music)
代码干净,复用性也很好。这就是面向组合的设计方式
第三章: 谈谈你对JS中this的理解
其实JS中的this是一个非常简单的东西,只需要理解它的执行规则就OK
在这里不想像其他博客一样展示太多的代码例子弄得天花乱坠, 反而不易理解
call/apply/bind可以显式绑定, 这里就不说了
主要这些场隐式绑定的场景讨论:
- 全局上下文
- 直接调用函数
- 对象.方法的形式调用
- DOM事件绑定(特殊)
- new构造函数绑定
- 箭头函数
1. 全局上下文
全局上下文默认this指向window, 严格模式下指向 undefined
2. 直接调用函数
let obj = {
a: function() {
console.log(this);
}
}
let func = obj.a;
func();
这种情况是直接调用。this相当于全局上下文的情况
3. 对象.方法的形式调用
还是刚刚的例子,我如果这样写:
obj.a();
这就是 对象.方法() 的情况,this指向这个对象
4. DOM事件绑定
onclick和addEventerListener中 this 默认指向绑定事件的元素
IE比较奇异,使用attachEvent,里面的this默认指向window
5. new+构造函数
此时构造函数中的this指向实例对象
6. 箭头函数?
箭头函数没有this, 因此也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。比如:
let obj = {
a: function() {
let do = () => {
console.log(this);
}
do();
}
}
obj.a(); // 找到最近的非箭头函数a,a现在绑定着obj, 因此箭头函数中的this是obj
优先级: new > call、apply、bind > 对象.方法 > 直接调用
第四章: 能不能模拟实现一个new的效果?
new 被调用后做了三件事情:
- 让实例可以访问到私有属性
- 让实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性
- 如果构造函数返回的结果不是引用数据类型
function newOperator(ctor, ...args) {
if (typeof ctor !== 'function') {
throw 'newOperator function the first param must be a function'
}
let obj = Object.create(ctor.prototype)
let res = ctor.apply(obj, args)
let isObject = res !== null && typeof res === 'object'
let isFunction = typeof res === 'function'
return isObect || isFunction ? res : obj
};
第五章: 能不能模拟实现一个 bind 的效果?
实现bind之前,我们首先要知道它做了哪些事情
- 对于普通函数,绑定this指向
- 对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.bind = function (context, ...args) {
// 异常处理
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
}
var self = this // 保存this的值,它代表调用 bind 的函数
var fNOP = function () { }
fNOP.prototype = this.prototype
var bound = function () {
//这就话的意思是:如果new的话就使用this,函数调用的话就用context
console.log(this, self, this instanceof self,"输出指向")
self.apply(this instanceof self ?
this :
context, args.concat(Array.prototype.slice.call(arguments)))
}
bound.prototype = new fNOP()
return bound
}
//—————————— 开始测试 ——————————
function A(params) { console.log(this, "输出this") } //{ name: '一缕清风' } 输出this
console.dir(A.bind({ name: "一缕清风" }))
/*
<- ƒ bound()
arguments: null
caller: null
length: 0
name: "bound"
prototype: A
__proto__:
constructor: ƒ A(params)
arguments: null
caller: null
length: 1
name: "A"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: VM3559:22
[[Scopes]]: Scopes[1]
__proto__: Object
__proto__: ƒ ()
[[FunctionLocation]]: VM3559:12
[[Scopes]]: Scopes[3]
*/
A.bind({ name: "一缕清风" })() //{name: "一缕清风"} "输出this"
var AAA = A.bind({ name: "一缕清风" })
new AAA() //bound {} "输出this"
也可以这么用 Object.create 来处理原型:
Function.prototype.bind = function (context, ...args) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var fbound = function () {
self.apply(this instanceof self ?
this :
context, args.concat(Array.prototype.slice.call(arguments)));
}
fbound.prototype = Object.create(this.prototype);
return fbound;
}
第六章: 能不能实现一个 call/apply 函数?
Function.prototype.call = function (obj) {
let context = obj
let fn = Symbol('fn')
context.fn = this
let args = []
for (let i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']')
}
let result = eval('context.fn(' + args + ')')
delete context.fn
return result
}
let obj = { name: "obj" };
function func(x, y) {
console.log(this)
return x + y
}
console.log(func.call(obj, 10, 20))
不过我认为换成 ES6 的语法会更精炼一些:
Function.prototype.call = function (obj, ...args) {
var context = obj || window;
let fn = Symbol('fn');
context.fn = this;
let result = eval('context.fn(...args)');
delete context.fn
return result;
}
let obj = { name: "obj" };
function func(x, y) {
console.log(this)
return x + y
}
console.log(func.call(obj, 10, 20))
类似的,有apply的对应实现:
Function.prototype.apply = function (obj, args) {
let context = obj || window;
context.fn = this;
let result = eval('context.fn(...args)');
delete context.fn
return result;
}
let obj = { name: "obj" };
function func(x, y) {
console.log(this)
return x + y
}
console.log(func.apply(obj, [10, 20]))
第七章:实现 Object.create
Object.create2 = function (proto, propertyObject = undefined) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null.')
if (propertyObject == null) {
new TypeError('Cannot convert undefined or null to object')
}
function F() { }
F.prototype = proto
const obj = new F()
if (propertyObject != undefined) {
Object.defineProperties(obj, propertyObject)
}
if (proto === null) {
// 创建一个没有原型对象的对象,Object.create(null)
obj.__proto__ = null
}
return obj
}
}
第八章:实现 Object.assign
Object.assign2 = function (target, ...source) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object')
}
let ret = Object(target)
source.forEach(function (obj) {
if (obj != null) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = obj[key]
}
}
}
})
return ret
}