笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。
第一章: 谈谈你对原型链的理解
原型对象和构造函数有何关系?
在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会自带一个prototype属性(显式原型),这个属性指向函数的原型对象 当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个**__proto__属性(隐式原型)**,指向构造函数的原型对象。
javascript
// 实例.__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对象为止,这样就形成了一个原型指向的链条, 即原型链
javascript
实例对象.__proto__ = 构造函数的.prototype
javascript
class A{}
class B extends A{}
class C extends B{}
console.log(new C())
对象的hasOwnProperty() 和 in 的区别?
- 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性。执行直接对象查找时,它始终不会查找原型。
- 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
javascript
var obj = {name:'一缕清风'}
'toString' in obj // true
'hasOwnProperty' in obj // true
obj.hasOwnProperty('toString') // false
⚠️ **注意: **虽然 in 能检测到原型链的属性,但 for in 通常却不行。
Function 原型链
javascript
console.log(Function.prototype); // [Function]
console.log(Object.getPrototypeOf(Function)); // [Function]
console.log(Function.__proto__); // [Function]
console.log(Function.__proto__.__proto__); // {}
console.log(Function.__proto__.__proto__.__proto__); //null
第二章: JS如何实现继承?
第一种: 借助call,实现构造函数继承
javascript
<script>
function Parent(age) {
this.name = 'parent'
this.age = age
}
Parent.prototype.say = function () {
console.log("Hello Word")
}
function Child(age) {
Parent.call(this, age) //继承Parent的属性值,注意:属性名相同情况下,遵循后者覆盖前者的规则
this.type = 'child'
}
console.log(new Parent()) //Parent {name: 'parent', age: undefined}
console.log(new Child(18)) //Child {name: 'parent', age: 18, type: 'child'}
</script>
这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法
第二种: 借助原型链
javascript
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' }
看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:
javascript
<script>
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, s1.play);
console.log(s2, s2.play);
</script>
明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。 那么还有更好的方式么?
第三种: 将前两种组合
javascript
<script>
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3]
}
function Child() {
Parent.call(this)
this.type = 'child';
}
Child.prototype = new Parent();
var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1, s1.play);
console.log(s2, s2.play);
</script>
之前的问题都得以解决。但是这里又徒增了一个新问题 那就是 Parent3 的构造函数会多执行了一次 Child3.prototype = new Parent3() 这是我们不愿看到的。那么如何解决这个问题?
第四种: 组合继承的优化1
javascript
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' }
这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问 。 :::warning 但是我们来测试的时候发现子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4 :::
第五种:(最推荐使用): 寄生组合继承的优化2
这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承
javascript
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 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__)) //{}
寄生式组合继承写法上和组合继承基本类似,区别是如下这里:
diff
- Dog.prototype = new Parent()
- Child.prototype.constructor = Child
+ function F() {}
+ F.prototype = Parent.prototype
+ let f = new F()
+ f.constructor = Child
+ Child.prototype = f
javascript
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] }
javascript
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
let f = new F()
f.constructor = c
c.prototype = f
}
})()
//------------------------------------------------------------------
//如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承:
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
javascript
var _inherit = (function () {
var F = function () { };
return function (c, p) {
c.uber = p.prototype;
F.prototype = p.prototype;
c.prototype = new F();
c.prototype.constructor = c;
}
})();
function Person() { this.name = "person" }
Person.prototype.say = function () {
console.log("Hello Word," + this.name)
}
function Student() { Person.call(this); this.name = "一缕清风" }
_inherit(Student, Person)
let superSay = Student.prototype.say
Student.prototype.say = function () {
console.time("耗时")
superSay.call(this)
console.timeEnd("耗时")
}
let s = new Student()
s.say()
第六种:借助 Object.create + Object.getPrototypeOf
javascript
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让一些不支持新语法的浏览器也能运行。 那最后编译成了什么样子呢?
javascript
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 实现继承
javascript
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这三个方法。
javascript
class Car {
constructor(id) {
this.id = id;
}
drive() {
console.log("drive!");
}
music() {
console.log("music!")
}
addOil() {
console.log("addOil")
}
}
class NewEnergyCar extends Car { };
const ncar = new NewEnergyCar();
ncar.addOil()
现在可以实现车的功能,并且以此去扩展不同的车。 但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。 如果让新能源汽车的类继承Car的话,也是有问题的,俗称"大猩猩和香蕉"的问题。大猩猩手里有香蕉,但是我现在明明只需要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了
继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承
当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复 ,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好
那如何来解决继承的诸多问题呢? 用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。 顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。
javascript
<script>
function drive() { console.log("drive!") }
function music() { console.log("music!") }
function addOil() { console.log("addOil") }
function compose(...args) {
return function () {
// args.reverse().reduce((previousValue, currentValue) => {
return args.reduceRight((previousValue, currentValue) => {
console.log(previousValue, arguments);
currentValue.apply(this, previousValue)
}, null)
}
}
/*
function compose() {
// var args = Array.from(arguments) // 转为数组类型
// var args = Array.prototype.slice.call(arguments) //转为数组类型
arguments.__proto__ = Array.prototype; //还是arguments类型,知识可以使用数组的方法
var args = arguments
return function () {
args.reverse().reduce((previousValue, currentValue) => {
console.log(previousValue, arguments);
currentValue.apply(this, arguments)
}, "")
}
}
*/
let car = compose(drive, music, addOil)
let newEnergyCar = compose(drive, music)
car()
</script>
代码干净,复用性也很好。这就是面向组合的设计方式
第三章: 谈谈你对JS中this的理解
其实JS中的this是一个非常简单的东西,只需要理解它的执行规则就OK 在这里不想像其他博客一样展示太多的代码例子弄得天花乱坠, 反而不易理解 call/apply/bind可以显式绑定, 这里就不说了 主要这些场隐式绑定的场景讨论:
- 全局上下文
- 直接调用函数
- 对象.方法的形式调用
- DOM事件绑定(特殊)
- new构造函数绑定
- 箭头函数
1. 全局上下文
全局上下文默认this指向window, 严格模式下指向 undefined
2. 直接调用函数
html
<script>
let obj = {
a: function() {
console.log(this);
}
}
obj.a();
/*
{a: ƒ}a: ƒ ()[[Prototype]]: Object
*/
let func = obj.a;
func(); // Window
</script>
这种情况是直接调用。this相当于全局上下文的情况
3. 对象.方法的形式调用
还是刚刚的例子,我如果这样写:
javascript
obj.a();
这就是 对象.方法() 的情况,this指向这个对象
4. DOM事件绑定
onclick和addEventerListener中 this 默认指向绑定事件的元素 IE比较奇异,使用attachEvent,里面的this默认指向window
5. new+构造函数
此时构造函数中的this指向实例对象
6. 箭头函数?
箭头函数没有this, 因此也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。比如:
javascript
let obj = {
a: function() {
let do = () => {
console.log(this);
}
do();
}
}
obj.a(); // 找到最近的非箭头函数a,a现在绑定着obj, 因此箭头函数中的this是obj
优先级: new > call、apply、bind > 对象.方法 > 直接调用
第四章: 能不能模拟实现一个new的效果?
new操作符做了哪些事情?
- 创建一个javascript空对象 {}
- 将要实例化对象的原形链指向该对象原形
- 绑定该对象为this的指向
- 返回该对象
new 被调用后做了三件事情:
- 让实例可以访问到私有属性
- 让实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性
- 如果构造函数返回的结果不是引用数据类型,返回创建的对象,反之,返回引用数据类型
javascript
function newOperator(ctor, ...args) {
if (typeof ctor !== 'function') {
throw 'newOperator function the first param must be a function'
}
// function Fn() { }
// Fn.prototype = ctor.prototype
// let obj = new Fn()
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 isObject || isFunction ? res : obj
};
function Person(name, age) {
this.name = name
this.age = age
}
function fn(name, age) {
return name + ' ' + age
}
// console.log(new Person("一缕清风", 20)); //Person { name: '一缕清风', age: 20 }
console.log(newOperator(Person, "一缕清风", 20)); //Person { name: '一缕清风', age: 20 }
console.log(newOperator(fn, "一缕清风", 20)); // fn {}
javascript
function Person() { this.name = "一缕清风" }
function newOperator(ctor) {
var obj = Object(null)
obj.__proto__ = ctor.prototype
let res = ctor.call(obj)
let isObject = res !== null && typeof res === 'object'
let isFunction = typeof res === 'function'
return isObject || isFunction ? res : obj
}
console.log(newOperator(Person));
new 一个构造函数,如果函数有返回值,会发生什么情况?
:::tips 如果函数返回一个对象,那么new 调用这个函数的返回对象,否则返回 new 创建的新对象。 以下是函数返回 return {} 、 return null , return 1 , return true 等等的情况: :::
javascript
function Fn1() { return undefined }
console.log(new Fn1()); // Fn1 {}
function Fn2() { return null }
console.log(new Fn2()); // Fn2 {}
function Fn3() { return 1 }
console.log(new Fn3()); //Fn3 {}
function Fn4() { return true }
console.log(new Fn4()); //Fn4 {}
function Fn5() { return {} }
console.log(new Fn5()); //{}
function Fn6() { return [] }
console.log(new Fn6()); // []
function Fn7() { return Array }
console.log(new Fn7()); // [Function: Array]
function Fn8() { return Object }
console.log(new Fn8()); // [Function: Object]
function Fn9() { return Boolean }
console.log(new Fn9()); // [Function: Boolean]
function Fn10() { return Boolean(true) }
console.log(new Fn10()); // Fn10 {}
function Fn11() { return String(true) }
console.log(new Fn11()); // Fn11 {}
第五章: 能不能模拟实现一个 bind 的效果?
实现bind之前,我们首先要知道它做了哪些事情
- 对于普通函数,绑定this指向
- 对于构造函数,要保证原函数的原型对象上的属性不能丢失
javascript
<script>
Function.prototype.bindDiy = function (context, ...args) {
// // 异常处理
// if (typeof this !== "function") {
// throw new Error("Function.prototype.bindDiy - what is trying to be bound is not callable")
// }
var self = this // 保存this的值,它代表调用 bindDiy 的函数
var bound = function () {
console.log(this, "this")
console.log(self, "self")
console.log(context, "context")
//这就话的意思是:如果new的话就使用this,函数调用的话就用context
// var isUseNew = this instanceof self // ✅
var isUseNew = Object.getPrototypeOf(this) === bound.prototype
console.log(isUseNew, "是否使用new出对象")
self.apply(isUseNew ? this : context,
[...args.concat(Array.prototype.slice.call(arguments)),
// context
])
}
// var fNOP = function () { }
// fNOP.prototype = this.prototype
// bound.prototype = new fNOP()
// bound.prototype = Object.create(this.prototype);
Object.setPrototypeOf(bound.prototype, self.prototype)
return bound
}
//------------------------------ 开始测试 ------------------------------
function A() {
console.log(this, "this这是初始函数")
console.log(arguments, "初始函数arguments")
}
A.bindDiy({ name: "一缕清风" })()
console.log(`--------------------------------------------------------------------------------`)
function B(params) {
console.log(this, "this这是初始函数")
console.log(arguments, "初始函数arguments")
} //{ name: '一缕清风' } 输出this
B.prototype.say = function () { console.log(this, this.name + ":说话"); }
var BYD = B.bindDiy({ name: "一缕清风" }, 1, 2, 3, 4)
var byd = new BYD()
byd.say() //说话
console.log("自定义的new B.bindDiy()", byd);
console.log(`--------------------------------------------------------------------------------`)
console.log(`对比原生bind`)
var BB = B.bind({ name: "一缕清风" }, 1, 2, 3, 4)
var bb = new BB()
bb.say() //说话
console.log("原生new B.bind()", bb);
</script>
也可以这么用 Object.create 来处理原型:
javascript
<script>
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 bound = function () {
self.apply(this instanceof self ?
this :
context, args.concat(Array.prototype.slice.call(arguments)));
}
bound.prototype = Object.create(this.prototype);
return bound;
}
</script>
第六章: 能不能实现一个 call/apply 函数?
javascript
<script>
Function.prototype.call = function (obj) {
let context = obj
// let fn = Symbol('fn')
// 保存this的值,它代表调用 bind 的函数
context.fn = this
console.log(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))
</script>
不过我认为换成 ES6 的语法会更精炼一些:
javascript
<script>
Function.prototype.call = function (obj, ...args) {
var context = obj || window;
// let fn = Symbol('fn');
// 保存this的值,它代表调用 bind 的函数
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))
</script>
类似的,有apply的对应实现:
javascript
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
javascript
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
javascript
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
}
参考文献
:::info 类与对象篇(1) :::