事件循环 eventLoop篇
在Job queue中的队列分为两种类型:macro-task和microTask。我们举例来看执行顺序的规定,我们设
macro-task队列包含任务: a1, a2 , a3 micro-task队列包含任务: b1, b2 , b3
执行顺序为,首先执行marco-task队列开头的任务,也就是 a1 任务,执行完毕后,在执行micro-task队列里的所有任务,也就是依次执行b1, b2 , b3,执行完后清空micro-task中的任务,接着执行marco-task中的第二个任务,依次循环。
了解完了macro-task和micro-task两种队列的执行顺序之后,我们接着来看,真实场景下这两种类型的队列里真正包含的任务(我们以node V8引擎为例),在node V8中,这两种类型的真实任务顺序如下所示:
macro-task队列真实包含任务:
script(主程序代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
micro-task队列真实包含任务: process.nextTick, Promises, Object.observe, MutationObserver
由此我们得到的执行顺序应该为:
script(主程序代码)—>process.nextTick—>Promises...——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering
在ES6中macro-task队列又称为ScriptJobs,而micro-task又称PromiseJobs
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');
// start
// promise1
// timer1
// promise2
// timer2
// 宏任务是 一个一个执行的,微任务是一起执行的,所以 timer2 在push进了下一轮的宏任务中
// node 中 还不同 具体可以再次进行查看
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
// 1
// Promise {<fulfilled>: undefined}
// Promise.resolve方法的参数如果是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolved,Promise.resolve方法的参数,会同时传给回调函数。
// then方法接受的参数是函数,而如果传递的并非是一个函数,它实际上会将其解释为then(null),这就会导致前一个Promise的结果会传递下面。
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
// 1
// 2
// Promise是可以链式调用的,由于每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般任务的链式调用一样return this。
// 上面的输出结果之所以依次打印出1和2,是因为resolve(1)之后走的是第一个then方法,并没有进catch里,所以第二个then中的res得到的实际上是第一个then的返回值。并且return 2会被包装成resolve(2),被最后的then打印输出2。
Promise.resolve().then(() => {
return new Error('error!!!')
}).then(res => {
console.log("then: ", res)
}).catch(err => {
console.log("catch: ", err)
})
// "then: " "Error: error!!!"
// 返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')也被包裹成了return Promise.resolve(new Error('error!!!')),因此它会被then捕获而不是catch。
const promise = Promise.resolve().then(() => {
return promise;
})
promise.catch(console.err)
// Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
// 这里其实是一个坑,.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log) //并且这里没有错误
// 1
// 看到这个题目,好多的then,实际上只需要记住一个原则:.then 或.catch 的参数期望是函数,传入非函数则会发生值透传。
// 第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1) 的值直接传到最后一个then里,直接打印出1。
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).catch(err => {
console.log('catch', err)
})
// .then中接受两个参数,第一个是处理promise 成功的回调的函数,第二个是处理 失败的回调的参数
// 也就是说 例如 Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。
// 在本题中 错误直接被then的第二个参数捕获了,所以就不会被catch捕获了,输出结果为:error err!!!'
但是如果写成如下的代码(去掉 then中的第二个函数)输出结果就会是catch
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}).catch(err => {
console.log('catch', err)
})
// catch err!!!
还有一种情况如下
Promise.resolve()
.then(function success (res) {
throw new Error('error!!!')
}, function fail1 (err) {
console.log('fail1', err)
}).catch(function fail2 (err) {
console.log('fail2', err)
})
// fail2 Error: error!!!
// 在此代码中,在then的 第一个参数中就抛出了错误,那么就会直接进入到catch
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
// 1
// finally2
// finally
// finally2后面的then函数 2
// 这里考虑到 微任务的顺序问题也要考虑到finally的问题
// finally 主要记住以下几点就可以
// 1. finally() 不管 promise对象最后的状态如何都会执行
// 2. finally() 方法中不接受任何的参数, 也就是说你在finally中 是无法知道这个promise 最后是返回的是 resolve还是rejiect
// 3. 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。
// 4. finally本质上是then方法的特例
// 5. .finally()的错误捕获: 如下例子
Promise.resolve('1')
.finally(() => {
console.log('finally1')
throw new Error('我是finally中抛出的异常')
})
.then(res => {
console.log('finally后面的then函数', res)
})
.catch(err => {
console.log('捕获错误', err)
})
// 'finally1'
// '捕获错误' Error: 我是finally中抛出的异常
// 这里先执行 finally 然后抛出异常,对标上边第三点,返回异常的promise的对象,直接进入catch 所以
但是这里需要注意的是 这里是 抛出的异常, 如果是 返回的异常则结果完全不同,对标第三点
Promise.resolve('1')
.finally(() => {
console.log('finally1')
return new Error('我是finally中抛出的异常')
})
.then(res => {
console.log('finally后面的then函数', res)
})
.catch(err => {
console.log('捕获错误', err)
})
// 'finally1'
// 'finally后面的then函数' 1
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))
// 1
// 2
// 3
// [1, 2, 3]
// 首先每个函数执行,都会隔一秒打印出 对应值,然后 all 再输出一个数组,进行打印
// Promise.all来执行这个函数,执行的时候,看到一秒之后输出了1,2,3,同时输出了数组[1, 2, 3],三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。
但是Promise.all 的错误是直接处理掉的
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res => console.log(res))
.catch(err => console.log(err))
// // 1s后输出
// 1
// 3
// // 2s后输出
// 2
// Error: 2 这里是catch中捕获打印的
// // 4s后输出
// 4
// 后边就没有catch的
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log('result: ', res))
.catch(err => console.log(err))
// 1
// 'result: ' 1
// 2
// 3
// then只会捕获第一个成功的方法,其他的函数虽然还会继续执行,但是不是被then捕获了。
如果有错误,就不会再捕获其他的错误了
function runAsync(x) {
const p = new Promise(r =>
setTimeout(() => r(x, console.log(x)), 1000)
);
return p;
}
function runReject(x) {
const p = new Promise((res, rej) =>
setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
);
return p;
}
Promise.race([runReject(0), runAsync(1), runReject(2), runAsync(3)])
.then(res => console.log("result: ", res))
.catch(err => console.log(err));
// 0
// Error 0
// 1
// 3
// 2 // 这里的错误不会在捕获
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
// script start
// async1 start
// promise1
// script end
// 这里需要注意的是在async1中await后面的Promise是没有返回值的,也就是它的状态始终是pending状态,所以在await之后的内容是不会执行的,包括async1后面的 .then。
async函数中抛出了错误,就会终止错误结果,不会继续向下执行。
async function async1 () {
await async2();
console.log('async1');
return 'async1 success'
}
async function async2 () {
return new Promise((resolve, reject) => {
console.log('async2')
reject('error')
})
}
async1().then(res => console.log(res))
// async2
// Uncaught (in promise) error
// 如果想要让错误不足之处后面的代码执行,可以使用catch来捕获:
async function async1 () {
await Promise.reject('error!!!').catch(e => console.log(e))
console.log('async1');
return Promise.resolve('async1 success')
}
async1().then(res => console.log(res))
console.log('script start')
// script start
// error!!!
// async1
// async1 success
const async1 = async () => {
console.log('async1');
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then(res => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
// script start
// async1
// promise1
// script end
// 1
// 3
// timer2
// timer1
// 代码的执行过程如下:
// 1. 首先执行同步带吗,打印出script start;
// 2. 遇到定时器timer1将其加入宏任务队列;
// 3. 之后是执行Promise,打印出promise1,由于Promise没有返回值,所以后面的代码不会执行;
// 4. 然后执行同步代码,打印出script end;
// 5. 继续执行下面的Promise,.then和.catch期望参数是一个函数,这里传入的是一个数字,因此就会发生值渗透,将resolve(1)的值 6. 传到最后一个then,直接打印出1;
// 7. 遇到第二个定时器,将其加入到微任务队列,执行微任务队列,按顺序依次执行两个定时器,但是由于定时器时间的原因,会在两秒后先打印出timer2,在四秒后打印出timer1。
const p1 = new Promise((resolve) => {
setTimeout(() => {
resolve('resolve3');
console.log('timer1')
}, 0)
resolve('resovle1');
resolve('resolve2');
}).then(res => {
console.log(res) // resolve1
setTimeout(() => {
console.log(p1)
}, 1000)
}).finally(res => {
console.log('finally', res)
})
// resolve1
// finally undefined
// timer1
// Promise{<resolved>: undefined}
需要注意的是最后一个定时器打印出的p1其实是.finally的返回值,我们知道.finally的返回值如果在没有抛出错误的情况下默认会是上一个Promise的返回值,而这道题中.finally上一个Promise是.then(),但是这个.then()并没有返回值,所以p1打印出来的Promise的值会是undefined,如果在定时器的下面加上一个return 1,则值就会变成1。
27 题 主要是说明 setTimeout 里边算是单独的模块或者说单独的任务队列
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
1
// 7
// 6
// 8
// 2
// 4
// 3
// 5
// 9
// 11
// 10
// 12
(1)第一轮事件循环流程分析如下:
整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
遇到setTimeout,其回调函数被分发到宏任务Event Queue中。暂且记为setTimeout1。
遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process1。
遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。记为then1。
又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,记为setTimeout2。
宏任务Event Queue 微任务Event Queue
setTimeout1 process1
setTimeout2 then1
上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。发现了process1和then1两个微任务:
执行process1,输出6。
执行then1,输出8。
第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。
2)第二轮时间循环从**setTimeout1**宏任务开始:
首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。
new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
宏任务Event Queue 微任务Event Queue
setTimeout2 process2
then2
第二轮事件循环宏任务结束,发现有process2和then2两个微任务可以执行:
输出3。
输出5。
第二轮事件循环结束,第二轮输出2,4,3,5。
(3)第三轮事件循环开始,此时只剩setTimeout2了,执行。
直接输出9。
将process.nextTick()分发到微任务Event Queue中。记为process3。
直接执行new Promise,输出11。
将then分发到微任务Event Queue中,记为then3。
宏任务Event Queue 微任务Event Queue
process3
then3
第三轮事件循环宏任务执行结束,执行两个微任务process3和then3:
输出10。
输出12。
第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
30 题 主要说明 throw 出来的都是错误
Promise.resolve().then(() => {
console.log('1');
throw 'Error';
}).then(() => {
console.log('2');
}).catch(() => {
console.log('3');
throw 'Error';
}).then(() => {
console.log('4');
}).catch(() => {
console.log('5');
}).then(() => {
console.log('6');
});
// 1
// 3
// 5
// 6
// 在这道题目中,我们需要知道,无论是thne还是catch中,只要throw 抛出了错误,就会被catch捕获,如果没有throw出错误,就被继续执行后面的then。
Promise的状态在发生变化之后,就不会再发生变化。
this指向篇
juejin.cn/post/701947… juejin.cn/post/695904…
var obj = {
say: function() {
var f1 = () => {
console.log("1111", this);
}
f1();
},
pro: {
getPro:() => {
console.log(this);
}
}
}
var o = obj.say;
o();
obj.say();
obj.pro.getPro();
// 1111 window对象
// 1111 obj对象
// window对象
window.number = 2;
var obj = {
number: 3,
db1: (function(){
console.log(this);
this.number *= 4;
return function(){
console.log(this);
this.number *= 5;
}
})()
}
var db1 = obj.db1;
db1();
obj.db1();
console.log(obj.number); // 15
console.log(window.number); // 40
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
};
obj.method(fn, 1);
// 10 2
function a(xx){
this.x = xx;
return this
};
var x = a(5);
var y = a(6);
console.log(x.x) // undefined
console.log(y.x) // 6
function foo(something){
this.a = something
}
var obj1 = {}
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3
1. 在严格模式下,浏览器环境下,函数中的this是undefined, 但是window不受影响
"use strict";
var foo = 123;
function print(){
console.log('print this is ', this);
console.log(window.foo)
console.log(this.foo);
}
console.log('global this is ', this);
print();
// global this is Window{...}
// print this is undefined
// 123
// Uncaught TypeError: Cannot read property 'foo' of undefined
2. let/const定义的变量存在暂时性死区,而且不会挂载到window对象上
let a = 1;
const b = 2;
var c = 3;
function print() {
console.log(this.a);
console.log(window.a);
console.log(this.b);
console.log(this.c);
console.log(window.c);
}
print();
console.log(this.a);
// undefined
// undefined
// undefined
// 3
// 3
// undefined
3. 立即执行函数this指向window,创建后会马上执行且只会执行一次,不回被 bind apply call
var obj = {
b: 22,
fn: (function(){
console.log(this);
console.log(this.b)
}())
}
obj.fn() // 报错 fn 不是一个函数
// (function(){
// console.log(this);
// console.log(this.b)
// }()).bind({b: 33}) // 报错没有bind方法
4. 函数存储是一个引用地址,如果这个地址被其他的引用,它的this会发生变化
var obj = {
a: 1,
foo() {
console.log(this.a)
}
};
var a = 2;
var foo = obj.foo;
var obj2 = { a: 3, foo: obj.foo }
obj.foo();
foo();
obj2.foo();
// 1 2 3
// obj2.foo指向了obj.foo的堆内存,此后执行与obj无关(除非使用call/apply改变this指向)
例如另一个例子, 函数作为参数进行传递
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
// Window {…}
// 2
// 函数预编译四部曲前两步分别是:
// 找形参和变量声明,值赋予undefined
// 将形参与实参相统一,也就是将实参的值赋予形参。
// obj.foo作为实参,在预编译时将其值赋值给形参fn,是将obj.foo指向的地址赋给了fn,此后fn执行不会与obj产生任何关系。fn为默认绑定。
在将上边的题目变形
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }
obj2.doFoo(obj.foo)
// {a: 3, doFoo: ƒ}
// 2
// console.log(this): obj2.doFoo符合xxx.fn格式,doFoo的为隐式绑定,this为obj2,打印{a: 3, doFoo: ƒ}
// fn(): 没有于obj2产生联系,默认绑定,打印2
在回调函数中更要注意
var name='zcxiaobao';
function introduce(){
console.log('Hello,My name is ', this.name);
}
const Tom = {
name: 'TOM',
introduce: function(){
setTimeout(function(){
console.log(this)
console.log('Hello, My name is ',this.name);
})
}
}
const Mary = {
name: 'Mary',
introduce
}
const Lisa = {
name: 'Lisa',
introduce
}
Tom.introduce();
setTimeout(Mary.introduce, 100);
setTimeout(function(){
Lisa.introduce();
},200);
// setTimeout是异步调用的,只有当满足条件并且同步代码执行完毕后,才会执行它的回调函数。
Tom.introduce()执行: console位于setTimeout的回调函数中,回调函数的this指向window
Mary.introduce直接作为setTimeout的函数参数(类似题目,上边举例的题目),会发生隐式绑定丢失,this为默认绑定
Lisa.introduce执行虽然位于setTimeout的回调函数中,但保持xxx.fn模式,this为隐式绑定。
// 所以如果我们想在setTimeout或setInterval中使用外界的this,需要提前存储一下,避免this的丢失。
const Tom = {
name: 'TOM',
introduce: function(){
_self = this
setTimeout(function(){
console.log('Hello, My name is ',_self.name);
})
}
}
Tom.introduce()
5. 注意显示绑定比如call在setTimeout中的使用,其实还是要明白 setTimeout的fn参数的原理
var obj1 = {
a: 1
}
var obj2 = {
a: 2,
bar: function () {
console.log(this.a)
},
foo: function () {
setTimeout(function () {
console.log(this)
console.log(this.a)
}.call(obj1), 0)
}
}
var a = 3
obj2.bar()
obj2.foo()
// setTimeout(fn) {
// if (回调条件满足) (
// fn
// )
// }
修改了回调函数内fn的this指向
// 2
// {a: 1}
// 1
6. 注意正确使用call,call之前应该是个函数 (这里注意bind)
function foo () {
console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj) //
// 2
// 1
// 2
// Uncaught TypeError: Cannot read property 'call' of undefined
这里需要注意bind
bind 不会执行,只有绑定并返回一个新函数,所以如果写成 foo().bind(obj) 不会报错
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo()
foo.bind(obj)
foo().bind(obj)
7.使用call 要注意参数的位置
var obj = {
a: 1,
foo: function (b) {
b = b || this.a
return function (c) {
console.log(this.a + b + c)
}
}
}
var a = 2
var obj2 = { a: 3 }
obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
// obj.foo(a).call(obj2, 1):
// obj.foo(a): foo的AO中b值为传入的a(形参与实参相统一),值为2,返回匿名函数fn
// 匿名函数fn.call(obj2, 1): fn的this指向为obj2,c值为1
// this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6
// obj.foo.call(obj2)(1):
// obj.foo.call(obj2): obj.foo的this指向obj2,未传入参数,b = this.a = obj2.a = 3;返回匿名函数fn
// 匿名函数fn(1): c = 1,默认绑定,this指向window
// this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6
// 6
// 6
8. new绑定中的 return的新函数
function User (name, age) {
this.name = name;
this.age = age;
this.introduce = function () {
console.log(this.name)
}
this.howOld = function () {
return function () {
console.log(this.age)
}
}
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24)
zc.introduce()
zc.howOld()()
// 这个题很难不让人想到如下代码,都是函数嵌套,具体解法是类似的,可以对比来看一下啊。
const User = {
name: 'zc';
age: 18;
introduce = function () {
console.log(this.name)
}
howOld = function () {
return function () {
console.log(this.age)
}
}
}
var name = 'Tom';
var age = 18;
User.introduce()
User.howOld()()
// zc.introduce(): zc是new创建的实例,this指向zc,打印zc
// zc.howOld()(): zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18(阿包永远18)
// zc
// 18
9. new的 难点 (重要的是优先级)
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();
/*
预编译
GO = {
Foo: fn(Foo),
getName: function getName(){ console.log(5) };
}
分析后续执行
Foo.getName(): 执行Foo上的getName方法,打印2
getName(): 执行GO中的getName方法,打印4
Foo().getName()
Foo()执行
// 修改全局GO的getName为function(){ console.log(1); }
getName = function(){ console.log(1) }
// Foo为默认绑定,this -> window
// return window
return this
复制代码
Foo().getName(): 执行window.getName(),打印1
getName(): 执行GO中的getName,打印1
分析后面三个打印结果之前,先补充一些运算符优先级方面的知识(图源:MDN)
从上图可以看到,部分优先级如下:new(带参数列表) = 成员访问 = 函数调用 > new(不带参数列表)
!!!!!!!!!!!!!!new Foo.getName()
首先从左往右看:new Foo属于不带参数列表的new(优先级19),Foo.getName属于成员访问(优先级20),getName()属于函数调用(优先级20),同样优先级遵循从左往右执行。
Foo.getName执行,获取到Foo上的getName属性
此时原表达式变为new (Foo.getName)(),new (Foo.getName)()为带参数列表(优先级20),(Foo.getName)()属于函数调用(优先级20),从左往右执行
new (Foo.getName)()执行,打印2,并返回一个以Foo.getName()为构造函数的实例
这里有一个误区:很多人认为这里的new是没做任何操作的的,执行的是函数调用。那么如果执行的是Foo.getName(),调用返回值为undefined,new undefined会发生报错,并且我们可以验证一下该表达式的返回结果。
console.log(new Foo.getName())
// 2
// Foo.getName {}
复制代码
可见在成员访问之后,执行的是带参数列表格式的new操作。
!!!!!!!!!!!!!!new Foo().getName()
同步骤4一样分析,先执行new Foo(),返回一个以Foo为构造函数的实例
Foo的实例对象上没有getName方法,沿原型链查找到Foo.prototype.getName方法,打印3
!!!!!!!!!!!!!!new new Foo().getName()
从左往右分析: 第一个new不带参数列表(优先级19),new Foo()带参数列表(优先级20),剩下的成员访问和函数调用优先级都是20
new Foo()执行,返回一个以Foo为构造函数的实例
在执行成员访问,Foo实例对象在Foo.prototype查找到getName属性
执行new (new Foo().getName)(),返回一个以 Foo.prototype.getName()为构造函数的实例,打印3
new Foo.getName() 与 new new Foo().getName()区别:
new Foo.getName()的构造函数是Foo.getName
new new Foo().getName()的构造函数为Foo.prototype.getName
测试结果如下:
foo1 = new Foo.getName()
foo2 = new new Foo().getName()
console.log(foo1.constructor)
console.log(foo2.constructor)
复制代码
输出结果:
2
3
ƒ (){ console.log(2); }
ƒ (){ console.log(3); }
复制代码
通过这一步比较应该能更好的理解上面的执行顺序。
答案
2
4
1
1
2
3
3
*/
10. 综合题 作用域、闭包和this
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
// fn.call(null) 或者 fn.call(undefined) 都相当于fn()
// obj.fn为立即执行函数: 默认绑定,this指向window
// 我们来一句一句的分析:
// var number: 立即执行函数的AO中添加number属性,值为undefined
// this.number *= 2: window.number = 10
// number = number * 2: 立即执行函数AO中number值为undefined,赋值后为NaN
// number = 3: AO中number值由NaN修改为3
// 返回匿名函数,形成闭包
// 此时的obj可以类似的看成以下代码(注意存在闭包):
obj = {
number: 3,
fn: function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
}
myFun.call(null): 相当于myFun(),隐式绑定丢失,myFun的this指向window。
// var num = this.number: this指向window,num = window.num = 10
// this.number *= 2: window.number = 20
// console.log(num): 打印10
// number *= 3: 当前AO中没有number属性,沿作用域链可在立即执行函数的AO中查到number属性,修改其值为9
// console.log(number): 打印立即执行函数AO中的number,打印9
// obj.fn(): 隐式绑定,fn的this指向obj
// 继续一步一步的分析:
// var num = this.number: this->obj,num = obj.num = 3
// this.number *= 2: obj.number *= 2 = 6
// console.log(num): 打印num值,打印3
// number *= 3: 当前AO中不存在number,继续修改立即执行函数AO中的number,number *= 3 = 27
// console.log(number): 打印27
// console.log(window.number): 打印20
// 这里解释一下,为什么myFun.call(null)执行时,找不到number变量,是去找立即执行函数AO中的number,而不是找window.number: JavaScript采用的静态作用域,当定义函数后,作用域链就已经定死。(更详细的解释文章最开始的推荐中有)
// 10
// 9
// 3
// 27
// 20
promise相关
Promise是ES6引入的新特性,旨在解决回调地狱。
- 同步promise
- Promise存在三个状态:pending(等待态)、fulfilled(成功态)、rejected(失败态)
- pending为初始态,并可以转化为fulfilled和rejected
- 成功时,不可转为其他状态,且必须有一个不可改变的值(value)
- 失败时,不可转为其他状态,且必须有一个不可改变的原因(reason)
- new Promise(excutor = (resolve, reject) => { resolve(value) })将状态置为 fulfilled
- new Promise(excutor = (resolve, reject) => { reject(reson) })reject(reson)将状态置为 rejected
- 若是executor运行异常执行reject()
- thenable:then(onFulfilled, onRejected) onFulfilled:status为fulfilled,执行onFulfilled,传入value onRejected:status为rejected,执行onRejected,传入reason 示例代码 // 1.Promise存在三个状态:pending(等待态)、fulfilled(成功态)、rejected(失败态)
promisesaplus.com/ github.com/fortheallli…
1. promise中 then catch 的返回值不能是promise的本身,会造成死循环
const promise = Promise.resolve().then(() => {
return promise;
})
promise.catch(console.err)
// Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
// 这里其实是一个坑,.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
2. 如果一个错误被then中的reject 捕获,那么他不会再被 catch捕获
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).catch(err => {
console.log('catch', err)
})
// .then中接受两个参数,第一个是处理promise 成功的回调的函数,第二个是处理 失败的回调的参数
// 也就是说 例如 Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。
// 在本题中 错误直接被then的第二个参数捕获了,所以就不会被catch捕获了,输出结果为:error err!!!'
但是如果写成如下的代码(去掉 then中的第二个函数)输出结果就会是catch
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}).catch(err => {
console.log('catch', err)
})
// catch err!!!
还有一种情况如下
Promise.resolve()
.then(function success (res) {
throw new Error('error!!!')
}, function fail1 (err) {
console.log('fail1', err)
}).catch(function fail2 (err) {
console.log('fail2', err)
})
// fail2 Error: error!!!
// 在此代码中,在then的 第一个参数中就抛出了错误,那么就会直接进入到catch
3. finally 是一种特殊的then,最终都会执行,不接受任何参数也不知道promise的状态,默认返回上次的promise的返回值
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
// 1
// finally2
// finally
// finally2后面的then函数 2
// 这里考虑到 微任务的顺序问题也要考虑到finally的问题
// finally 主要记住以下几点就可以
// 1. finally() 不管 promise对象最后的状态如何都会执行
// 2. finally() 方法中不接受任何的参数, 也就是说你在finally中 是无法知道这个promise 最后是返回的是 resolve还是rejiect
// 3. 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。
// 4. finally本质上是then方法的特例
// 5. .finally()的错误捕获: 如下例子
Promise.resolve('1')
.finally(() => {
console.log('finally1')
throw new Error('我是finally中抛出的异常')
})
.then(res => {
console.log('finally后面的then函数', res)
})
.catch(err => {
console.log('捕获错误', err)
})
// 'finally1'
// '捕获错误' Error: 我是finally中抛出的异常
// 这里先执行 finally 然后抛出异常,对标上边第三点,返回异常的promise的对象,直接进入catch 所以
但是这里需要注意的是 这里是 抛出的异常, 如果是 返回的异常则结果完全不同,对标第三点
Promise.resolve('1')
.finally(() => {
console.log('finally1')
return new Error('我是finally中抛出的异常')
})
.then(res => {
console.log('finally后面的then函数', res)
})
.catch(err => {
console.log('捕获错误', err)
})
// 'finally1'
// 'finally后面的then函数' 1
4. promsie.all 所有的函数都会执行,并且最后输出一个执行结果的数组,并且结果和函数的执行顺序是一致的。
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))
// 1
// 2
// 3
// [1, 2, 3]
// 首先每个函数执行,都会隔一秒打印出 对应值,然后 all 再输出一个数组,进行打印
// Promise.all来执行这个函数,执行的时候,看到一秒之后输出了1,2,3,同时输出了数组[1, 2, 3],三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。
但是Promise.all 的错误是直接处理掉的,
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res => console.log(res))
.catch(err => console.log(err))
// // 1s后输出
// 1
// 3
// // 2s后输出
// 2
// Error: 2 这里是catch中捕获打印的
// // 4s后输出
// 4
// 后边就没有catch的
5. promise.race 里边的函数都会执行,但是 只会捕获第一个成功的或失败的,状态就不会再改变了
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log('result: ', res))
.catch(err => console.log(err))
// 1
// 'result: ' 1
// 2
// 3
// then只会捕获第一个成功的方法,其他的函数虽然还会继续执行,但是不是被then捕获了。
如果有错误,就不会再捕获其他的错误了
function runAsync(x) {
const p = new Promise(r =>
setTimeout(() => r(x, console.log(x)), 1000)
);
return p;
}
function runReject(x) {
const p = new Promise((res, rej) =>
setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
);
return p;
}
Promise.race([runReject(0), runAsync(1), runReject(2), runAsync(3)])
.then(res => console.log("result: ", res))
.catch(err => console.log(err));
// 0
// Error 0
// 1
// 3
// 2 // 这里的错误不会在捕获
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
// 1s后输出
// 1
// 3
// // 2s后输出
// 2
// Error: 2
// // 4s后输出
// 4
es6-class 相关
class 继承
首先从 类的基本使用, 有哪些属性,哪些可以继承,哪些怎么用 然后 到 如何使用 类 来进行继承的
类继承的实现的原理是什么
手写类实现的原理
其他的比如: 1. 类和构造函数之间的区别
什么是类
类的本质还是一个函数,类就是构造函数的另一种写法。 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。 基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
ES6中类没有变量提升,必须用new调用
通过构造函数创建实例,是可以变量提升的。 es6中的类,必须先有类,才可以实例化 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
类的所有方法都定义在类的prototype属性上面
让我们来测试一下。
class Father{
constructor(name){
this.name = name;
}
sing(){
return this.name;
}
}
let red = new Father('小红');
let green = new Father('小绿');
console.log(red.sing === green.sing); //true
// 向类中添加方法
// 通过Object.assign,在原型上追加方法。
class Father{
constructor(name){
this.name = name;
}
sing(){
return this.name;
}
}
//在原型上追加方法
Object.assign(Father.prototype,{
dance(){
return '我爱跳舞';
}
});
let red = new Father('小红');
let green = new Father('小绿');
console.log(red.dance());//我爱跳舞
console.log(red.dance === green.dance); //true
class 类中定义的方法都是不可枚举的,但是ES5中的不是
另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
上面代码中,toString()方法是Point类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function () {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
上面代码采用 ES5 的写法,toString()方法就是可枚举的。
constructor方法
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
constructor()方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
上面代码中,constructor()函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
使用static关键字,作为静态方法(静态方法,只能通过类调用,实例不能调用)
class Foo {
static classMethod() {
return "hello";
}
}
Foo.classMethod(); // 'hello'
class 类的实例
- 与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
上面代码中,x和y都是实例对象point自身的属性(因为定义在this对象上),所以hasOwnProperty()方法返回true,而toString()是原型对象的属性(因为定义在Point类上),所以hasOwnProperty()方法返回false。这些都与 ES5 的行为保持一致。 与 ES5 一样,类的所有实例共享一个原型对象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
上面代码中,p1和p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的。 这也意味着,可以通过实例的__proto__属性为“类”添加方法。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
上面代码在p1的原型上添加了一个printName()方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。
取值函数(getter)和存值函数(setter)
与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。 存值函数和取值函数是设置在属性的 Descriptor 对象上的。
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(
CustomHTMLElement.prototype, "html"
);
"get" in descriptor // true
"set" in descriptor // true
上面代码中,存值函数和取值函数是定义在html属性的描述对象上面,这与 ES5 完全一致。
属性表达式
类的属性名,可以采用表达式。
let methodName = 'getArea';
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
上面代码中,Square类的方法名getArea,是从表达式得到的。
Class 表达式 与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代码表示,Me只在 Class 内部有定义。 如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。 const MyClass = class { /* ... */ };
采用 Class 表达式,可以写出立即执行的 Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"
上面代码中,person是一个立即执行的类的实例。
class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。 注意,如果静态方法包含this关键字,这个this指的是类,而不是实例
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
静态方法也是可以从super对象上调用的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
实例属性的新写法
实例属性this._count定义在constructor()方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。
class IncreasingCounter {
_count = 0;
// constructor() {
// this._count = 0;
// }
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。
静态属性
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
上面的写法为Foo类定义了一个静态属性prop。 目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
// 这个新写法大大方便了静态属性的表达。
// 老写法
class Foo {
// ...
}
Foo.prop = 1;
// 新写法
class Foo {
static prop = 1;
}
上面代码中,老写法的静态属性定义在类的外部。整个类生成以后,再生成静态属性。这样让人很容易忽略这个静态属性,也不符合相关代码应该放在一起的代码组织原则。另外,新写法是显式声明(declarative),而不是赋值处理,语义更好
私有方法和私有属性 (仅做记录不做学习)
现有的解决方案
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。 一种做法是在命名上加以区别。
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
上面代码中,_bar()方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。 另一种方法就是索性将私有方法移出类,因为类内部的所有方法都是对外可见的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
上面代码中,foo是公开方法,内部调用了bar.call(this, baz)。这使得bar()实际上成为了当前类的私有方法。 还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
上面代码中,bar和snaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。 const inst = new myClass();
Reflect.ownKeys(myClass.prototype) // [ 'constructor', 'foo', Symbol(bar) ]
上面代码中,Symbol 值的属性名依然可以从类的外部拿到。
私有属性的提案
目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。
class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}
上面代码中,#count就是私有属性,只能在类的内部使用(this.#count)。如果在类的外部使用,就会报错。
const counter = new IncreasingCounter();
counter.#count // 报错
counter.#count = 42 // 报错
上面代码在类的外部,读取私有属性,就会报错。 下面是另一个例子。
class Point {
#x;
constructor(x = 0) {
this.#x = +x;
}
get x() {
return this.#x;
}
set x(value) {
this.#x = +value;
}
}
上面代码中,#x就是私有属性,在Point类之外是读取不到这个属性的。由于井号#是属性名的一部分,使用时必须带有#一起使用,所以#x和x是两个不同的属性。 之所以要引入一个新的前缀#表示私有属性,而没有采用private关键字,是因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号似乎是唯一的比较方便可靠的方法,能够准确地区分一种属性是否为私有属性。另外,Ruby 语言使用@表示私有属性,ES6 没有用这个符号而使用#,是因为@已经被留给了 Decorator。 这种写法不仅可以写私有属性,还可以用来写私有方法。
class Foo {
#a;
#b;
constructor(a, b) {
this.#a = a;
this.#b = b;
}
#sum() {
return this.#a + this.#b;
}
printSum() {
console.log(this.#sum());
}
}
上面代码中,#sum()就是一个私有方法。 另外,私有属性也可以设置 getter 和 setter 方法。
class Counter {
#xValue = 0;
constructor() {
super();
// ...
}
get #x() { return #xValue; }
set #x(value) {
this.#xValue = value;
}
}
上面代码中,#x是一个私有属性,它的读写都通过get #x()和set #x()来完成。 私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性。
class Foo {
#privateValue = 42;
static getPrivateValue(foo) {
return foo.#privateValue;
}
}
Foo.getPrivateValue(new Foo()); // 42
上面代码允许从实例foo上面引用私有属性。 私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。
class FakeMath {
static PI = 22 / 7;
static #totallyRandomNumber = 4;
static #computeRandomNumber() {
return FakeMath.#totallyRandomNumber;
}
static random() {
console.log('I heard you like random numbers…')
return FakeMath.#computeRandomNumber();
}
}
FakeMath.PI // 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
// 4
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错
上面代码中,#totallyRandomNumber是私有属性,#computeRandomNumber()是私有方法,只能在FakeMath这个类的内部调用,外部调用就会报错。
in 运算符
try...catch结构可以用来判断是否存在某个私有属性。
class A {
use(obj) {
try {
obj.#foo;
} catch {
// 私有属性 #foo 不存在
}
}
}
const a = new A();
a.use(a); // 报错
上面示例中,类A并不存在私有属性#foo,所以try...catch报错了。 这样的写法很麻烦,可读性很差,V8 引擎改进了in运算符,使它也可以用来判断私有属性。
class A {
use(obj) {
if (#foo in obj) {
// 私有属性 #foo 存在
} else {
// 私有属性 #foo 不存在
}
}
}
上面示例中,in运算符判断当前类A的实例,是否有私有属性#foo,如果有返回true,否则返回false。 in也可以跟this一起配合使用。
class A {
#foo = 0;
m() {
console.log(#foo in this); // true
console.log(#bar in this); // false
}
}
注意,判断私有属性时,in只能用在定义该私有属性的类的内部。
class A {
#foo = 0;
static test(obj) {
console.log(#foo in obj);
}
}
A.test(new A()) // true
A.test({}) // false
class B {
#foo = 0;
}
A.test(new B()) // false
上面示例中,类A的私有属性#foo,只能在类A内部使用in运算符判断,而且只对A的实例返回true,对于其他对象都返回false。
子类从父类继承的私有属性,也可以使用in运算符来判断。
class A {
#foo = 0;
static test(obj) {
console.log(#foo in obj);
}
}
class SubA extends A {};
A.test(new SubA()) // true
上面示例中,SubA从父类继承了私有属性#foo,in运算符也有效。 注意,in运算符对于Object.create()、Object.setPrototypeOf形成的继承,是无效的,因为这种继承不会传递私有属性。
class A {
#foo = 0;
static test(obj) {
console.log(#foo in obj);
}
}
const a = new A();
const o1 = Object.create(a);
A.test(o1) // false
A.test(o1.__proto__) // true
const o2 = {};
Object.setPrototypeOf(o2, A);
A.test(o2) // false
A.test(o2.__proto__) // true
上面示例中,对于修改原型链形成的继承,子类都取不到父类的私有属性,所以in运算符无效。
new.target 属性 (仅做了解不做学习)
new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
上面代码确保构造函数只能通过new命令调用。 Class 内部调用new.target,返回当前 Class。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
var obj = new Rectangle(3, 4); // 输出 true
- 需要注意的是,子类继承父类时,new.target会返回子类。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length, width) {
super(length, width);
}
}
var obj = new Square(3); // 输出 false
上面代码中,new.target会返回子类。
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
上面代码中,Shape类不能被实例化,只能用于继承。 注意,在函数外部,使用new.target会报错。
class 类的注意点
1. 静态方法可以与非静态方法重名。
2. 严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式
3. 不存在变量提升
类不存在变量提升(hoist),这一点与 ES5 完全不同。 new Foo(); // ReferenceError class Foo {} 上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。 { let Foo = class {}; class Bar extends Foo { } } 上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。
name 属性
由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。 class Point {} Point.name // "Point" name属性总是返回紧跟在class关键字后面的类名。
Generator 方法
如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world
上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。
this 的指向
类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错。
在构造方法中绑定this
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
另一种解决方法是使用箭头函数。 (认为是最优解)
class Obj {
constructor() {
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myObj // true
箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。
还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this。 (了解)
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger = selfish(new Logger());
类的继承
一个简单的例子
class Father {
constructor(name){
this.name = name;
}
dance(){
return '我在跳舞';
}
}
class Son extends Father{
constructor(name,score){
super(name);
this.score = score;
}
sing(){
return this.name +','+this.dance();
}
}
let obj = new Son('小红',100);
console.log(obj.dance())
console.log(obj.sing())
基础
- extends 关键字
- 子类必须在constructor中调用 super 才可以
- 只有调用super之后,才可以使用this关键字,否则会报错
Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class Point {
}
class ColorPoint extends Point {
}
上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。 ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。 如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
- 子类的constructor方法没有调用super之前,就使用this关键字,结果报错 上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。 下面是生成子类实例的代码。
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与 ES5 的行为完全一致。 最后,父类的静态方法,也会被子类继承。
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
上面代码中,hello()是A类的静态方法,B继承A,也继承了A的静态方法。
Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类。 Object.getPrototypeOf(ColorPoint) === Point // true 因此,可以使用这个方法判断,一个类是否继承了另一个类。
super 关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
class A {}
class B extends A {
constructor() {
super();
}
}
上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。 注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
上面代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
class A {}
class B extends A {
m() {
super(); // 报错
}
}
上面代码中,super()用在B类的m方法之中,就会造成语法错误。
第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。
这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined
上面代码中,p是父类A实例的属性,super.p就引用不到它。 如果属性定义在父类的原型对象上,super就可以取到。
class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();
上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。
ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。
由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。
如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
class A {
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
}
}
B.x = 3;
B.m() // 3
上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。
注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(super.valueOf() instanceof B); // true
}
}
let b = new B();
上面代码中,super.valueOf()表明super是一个对象,因此就不会报错。同时,由于super使得this指向B的实例,所以super.valueOf()返回的是一个B的实例。 最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
类的 prototype 属性和__proto__属性
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。 (1)子类的__proto__属性,表示构造函数的继承,总是指向父类。 (2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
上面代码中,子类B的__proto__属性指向父类A,子类B的prototype属性的__proto__属性指向父类A的prototype属性。 这样的结果是因为,类的继承是按照下面的模式实现的。
class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
const b = new B();
《对象的扩展》一章给出过Object.setPrototypeOf方法的实现。
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
因此,就得到了上面的结果。
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。
B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
extends关键字后面可以跟多种类型的值。
class B extends A {
}
上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。
下面,讨论两种情况。第一种,子类继承Object类。
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。
第二种情况,不存在任何继承。
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。 实例的 proto 属性 子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
上面代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型。 因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
上面代码在ColorPoint的实例p2上向Point类添加方法,结果影响到了Point的实例p1。
原生构造函数的继承 (仅了解)
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
Boolean() Number() String() Array() Date() Function() RegExp() Error() Object()
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
上面代码定义了一个继承 Array 的MyArray类。但是,这个类的行为与Array完全不一致。
var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。 ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。 下面的例子中,我们想让一个普通对象继承Error对象。
var e = {};
Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]
Object.getOwnPropertyNames(e)
// []
上面代码中,我们想通过Error.call(e)这种写法,让普通对象e具有Error对象的实例属性。但是,Error.call()完全忽略传入的第一个参数,而是返回一个新对象,e本身没有任何变化。这证明了Error.call(e)这种写法,无法继承原生构造函数。 ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。 上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面就是定义了一个带版本功能的数组。
class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]
x.revert();
x // [1, 2]
上面代码中,VersionedArray会通过commit方法,将自己的当前状态生成一个版本快照,存入history属性。revert方法用来将数组重置为最新一次保存的版本。除此之外,VersionedArray依然是一个普通数组,所有原生的数组方法都可以在它上面调用。 下面是一个自定义Error子类的例子,可以用来定制报错时的行为。
class ExtendableError extends Error {
constructor(message) {
super();
this.message = message;
this.stack = (new Error()).stack;
this.name = this.constructor.name;
}
}
class MyError extends ExtendableError {
constructor(m) {
super(m);
}
}
var myerror = new MyError('ll');
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
// at MyError.ExtendableError
// ...
注意,继承Object的子类,有一个行为差异。(stackoverflow.com/questions/3…)
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。
Mixin 模式的实现 (仅做了解)
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
上面代码中,c对象是a对象和b对象的合成,具有两者的接口。 下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝静态属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
ES6 class 类的实现原理
基础类
(图片) 我们就会按照这个类,来回摩擦。然后再来分析编译后的代码。
"use strict";
function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} 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) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
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) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
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;
}
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) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} 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);
_defineProperty(this, "shili", '实例属性');
this.name = 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。
简易版实现class
ES6的 Class 内部是基于寄生组合式继承,它是目前最理想的继承方式 ES6的 Class 允许子类继承父类的静态方法和静态属性
// Child 为子类的构造函数, Parent为父类的构造函数
function selfClass(Child, Parent) {
// Object.create 第二个参数,给生成的对象定义属性和属性描述符/访问器描述符
Child.prototype = Object.create(Parent.prototype, {
// 子类继承父类原型上的属性和方法
constructor: {
enumerable: false,
configurable: false,
writable: true,
value: Child
}
});
// 继承父类的静态属性和静态方法
Object.setPrototypeOf(Child, Parent);
}
// 测试
function Child() {
this.name = 123;
}
function Parent() {}
// 设置父类的静态方法getInfo
Parent.getInfo = function() {
console.log("info");
};
Parent.prototype.getName = function() {
console.log(this.name);
};
selfClass(Child, Parent);
Child.getInfo(); // info
let tom = new Child();
tom.getName(); // 123
ES6的类继承 和 ES5的继承的区别
juejin.cn/post/684490… juejin.cn/post/700128… es6.ruanyifeng.com/#docs/class
原型链相关
本节要点
原型 原型链
__proto__ prototype constructor
构造函数
new Object.create()
typeof
instanceOf
ES5继承 和 ES6的类的使用和继承
new Object 和 let a = {} let b = Object.creat(null) 对象创建的几种方式
hasOwnproperty 判断对象是否存在某个属性 获取对象属性的方法 的区别
类型判断的方法总结
链接
js 类型判断
1. typeof
首先使用 typeof typeof 可以 检测出 非Object的所有非引用类型包括,number string symbol boolean undefined BigInt function
注意 typeof 出来的结果都是字符串
注意 null 你也可以自己判断
Object.prototype.toString.call(num)
- 注意 Set 也可以
var num = 123;
var str = 'abcdef';
var bool = true;
var arr = [1, 2, 3, 4];
var obj = {name:'wenzi', age:25};
var func = function(){ console.log('this is function'); }
var und = undefined;
var nul = null;
var date = new Date();
var reg = /^[a-zA-Z]{5,20}$/;
var error= new Error();
var set = new Set()
Object.prototype.toString.call(num), // '[object Number]'
Object.prototype.toString.call(str), // '[object String]'
Object.prototype.toString.call(bool), // '[object Boolean]'
Object.prototype.toString.call(arr), // '[object Array]'
Object.prototype.toString.call(obj), // '[object Object]'
Object.prototype.toString.call(func), // '[object Function]'
Object.prototype.toString.call(und), // '[object Undefined]'
Object.prototype.toString.call(nul), // '[object Null]'
Object.prototype.toString.call(date), // '[object Date]'
Object.prototype.toString.call(reg), // '[object RegExp]'
Object.prototype.toString.call(error) // '[object Error]'
Object.prototype.toString.call(set) // '[object Set]'
instanceof
instanceof 运算符是用来判断一个对象是否在其原型链原型构造函数的属性,所以在比较对象(引用类型)时才有意义
num instanceof Number, // false
str instanceof String, // false
bool instanceof Boolean, // false
arr instanceof Array, // true--注意
arr instanceof Object, // true--注意
obj instanceof Object, // true--注意
func instanceof Function, // true
und instanceof Object, // false
nul instanceof Object, // false
date instanceof Date, // true
reg instanceof RegExp, // true
error instanceof Error // true
从结果可以看出instanceof不能判断值类型,但是引用类型可以,值得注意的是arr和obj在instanceof Object的时候的值都是true,这就导致判断是对象时不准确
Array.isArray()
Array.isArray([1, 2, 3]); // true
Array.isArray({foo: 123}); // false
Array.isArray("foobar"); // false
Array.isArray(undefined); // false
对象创建的几种方式
一、通过对象字面量方式创建
var person = {
name: 'zhangsan',
age: 18
}
二、通过构造函数 缺点,内存消耗大,每个实例化的对象,方法引用新的空间地址
function Person(name, age) {
this.name = name
this.age = age
this.namePrint = function() {
console.log(this.name)
}
}
var obj = new Person(name, age)
改进方法一:将函数放到全局,解决内存损耗问题,缺点,造成全局污染
function Person(name, age) {
this.name = name
this.age = age
this.namePrint = namePrint
}
function namePrint() {
console.log(this.name)
}
var obj = new Person(name, age)
三、通过object方式创建
var person = new Object()
person.name = 'zhangsan'
person.age = 18
四、使用工厂模式创建对象 缺点:instanceof、prototype失效、浪费内存
function Person(name, age) {
var person = new Object()
person.name = name
person.age = age
person.namePrint = function() {
console.log(name)
}
return person
}
var person1 = new Person('zhangsan', 18)
var person2 = new Person('lisi', 19)
console.log(person1 instanceof Person) // false
console.log(person1.__proto__ == Person.prototype) // false
console.log(person1.namePrint === person2.namePrint) // false
五、通过原型模式创建对象
缺点:引用类型属性会被所有的实例对象共享并且修改
function Person() {}
Person.prototype.name = 'person'
Person.prototype.namePrint = function() {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
// 改进方法:原型继承组合模式
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person,
namePrint: function() {
console.log(this.name)
}
}
var p1 = new Person('zhangsan', 18)
判断对象中是否存在某属性
1. .或者 []
通过点或者方括号可以获取对象的属性值,如果对象上不存在该属性,则会返回 undefined 。
当然,这里的"不存在" 指的是对象自身和原型链上都不存在,如果原型链有该属性,则会返回原型链上的属性值。
// 创建对象
let test = {name : 'lei'}
// 获取对象的自身的属性
test.name //"lei"
test["name"] //"lei"
// 获取不存在的属性
test.age //undefined
// 获取原型上的属性
test["toString"] //toString() { [native code] }
// 新增一个值为undefined的属性
test.un = undefined
test.un //undefined 不能用在属性值存在,但可能为 undefined的场景
所以,我们可以根据 Obj.x !== undefined 的返回值 来判断Obj是否有x属性。 这种方式很简单方便,局限性就是:不能用在x的属性值存在,但可能为 undefined的场景。 in运算符可以解决这个问题
in 运算符
如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。
'name' in test //true
'un' in test //true
'toString' in test //true
'age' in test //false
示例中可以看出,值为undefined的属性也可正常判断。 这种方式的局限性就是无法区分自身和原型链上的属性,在只需要判断自身属性是否存在时,这种方式就不适用了。这时需要hasOwnProperty()
hasOwnProperty()
test.hasOwnProperty('name') //true 自身属性
test.hasOwnProperty('age') //false 不存在
test.hasOwnProperty('toString') //false 原型链上属性
可以看到,只有自身存在该属性时,才会返回true。适用于只判断自身属性的场景。
- 不支持create
o = Object.create(null)
o.hasOwnProperty('name'); // 返回 true
会直接报错:
Uncaught TypeError: o.hasOwnProperty is not a function at <anonymous>:1:3
- 覆盖报错 如果一个对象有一个自己的名为 name 的属性'hasOwnProperty',那么该属性将被覆盖
Object.prototype.hasOwnProperty并且我们无法访问它:
o={hasOwnProperty:"搞前端的半夏"}
o.hasOwnProperty('name');
直接报错
VM123:3 Uncaught TypeError: o.hasOwnProperty is not a function
at <anonymous>:3:3
Object.prototype.hasOwnProperty()
用法:Object.prototype.hasOwnProperty.call(obj, propName);,接受两个参数。请注意我们这里用到了call。改变this的指向。
Object.prototype.hasOwnProperty除了支持hasOwnProperty的相同用法,同时还解决了hasOwnProperty的两个缺点。
o={hasOwnProperty:"搞前端的半夏"}
Object.prototype.hasOwnProperty.call.hasOwnProperty(o,'name'); // 返回false
ES13(ES2022)Object.hasOwn()
Object.hasOwn(),目前来看就是替代Object.prototype.hasOwnProperty.call()。 如果用Object.prototype.hasOwnProperty.call() 来封装的话,代码如下:
function hasOwn(obj, propName) {
return Object.prototype.hasOwnProperty.call(obj, propName);
}
Object.hasOwn({name: '搞前端的半夏'}, 'name') // true
Object.hasOwn({}, 'name') //false
Object.hasOwn({}, 'toString') //false
Object.hasOwn(Object.create(null), 'name') //false
Object.hasOwn({hasOwnProperty: 'yes'}, 'name') //false
Object.hasOwn({hasOwn: 'yes'}, 'name') //false
函数对象和普通对象
万物皆对象。而我们都知道在 JavaScript 中,创建对象有好几种方式,比如对象字面量,或者直接通过构造函数 new 一个对象出来:
我们将对象分为函数对象和普通对象。所谓的函数对象,其实就是 JavaScript 的用函数来模拟的类实现。JavaScript 中的 Object 和 Function 就是典型的函数对象。 String、Array、Number
关于函数对象和普通对象,咱直接看代码:
function fun1(){};
const fun2 = function(){};
const fun3 = new Function('name','console.log(name)');
const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();
const a1 = new Number(1)
const a2 = new String('22')
console.log(typeof Object);//function
console.log(typeof Function);//function
console.log(typeof Array, Number, String) // function
console.log(typeof fun1);//function
console.log(typeof fun2);//function
console.log(typeof fun3);//function
console.log(typeof obj1);//object
console.log(typeof obj2);//object
console.log(typeof obj3);//object
console.log(typeof obj4);//object
console.log(typeof a1, a2) // object
所有 Function 的实例都是函数对象,其他的均为普通对象,其中包括 Function 实例的实例。
自己的想法: 也可以这么认为,除了 new Function 其他的new都是对象
JavaScript 中万物皆对象,而对象皆出自构造(构造函数)。
上图中,你疑惑的点是不是 Function 和 new Function 的关系。其实是这样子的:
Function.__proto__ === Function.prototype//true
__proto__
- 首先我们需要明确两点:1️⃣__proto__和constructor是对象独有的。2️⃣prototype属性是函数独有的; 但是在 JavaScript 中,函数也是对象,所以函数也拥有__proto__和 constructor属性。
结合上面我们介绍的 Object 和 Function 的关系,看一下代码和关系图
function Person(){…};
let nealyang = new Person();
image
再梳理上图关系之前,我们再来讲解下__proto__。
__proto__ 的例子,说起来比较复杂,可以说是一个历史问题。
ECMAScript 规范描述 prototype 是一个隐式引用,但之前的一些浏览器,已经私自实现了 __proto__这个属性,使得可以通过 obj.__proto__ 这个显式的属性访问,访问到被定义为隐式属性的 prototype。
因此,情况是这样的,ECMAScript 规范说 prototype 应当是一个隐式引用:
- 通过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象
- 通过 Object.setPrototypeOf(obj, anotherObj) 间接设置指定对象的 prototype 对象
- 部分浏览器提前开了
__proto__的口子,使得可以通过obj.__proto__直接访问原型,通过obj.__proto__ = anotherObj直接设置原型 ECMAScript 2015 规范只好向事实低头,将__proto__属性纳入了规范的一部分
从浏览器的打印结果我们可以看出,上图对象 a 存在一个__proto__属性。而事实上,他只是开发者工具方便开发者查看原型的故意渲染出来的一个虚拟节点。虽然我们可以查看,但实则并不存在该对象上。
__proto__属性既不能被 for in 遍历出来,也不能被 Object.keys(obj) 查找出来。
访问对象的 obj.__proto__ 属性,默认走的是 Object.prototype 对象上 __proto__ 属性的 get/set 方法。
Object.defineProperty(Object.prototype,'__proto__',{
get(){
console.log('get')
}
});
({}).__proto__;
console.log((new Object()).__proto__);
关于更多__proto__更深入的介绍,可以参看工业聚大佬的《深入理解 JavaScript 原型》一文。(mp.weixin.qq.com/s/1UDILezro…) 这里我们需要知道的是,__proto__是对象所独有的,并且__proto__是一个对象指向另一个对象,也就是他的原型对象。我们也可以理解为父类对象。它的作用就是当你在访问一个对象属性的时候,如果该对象内部不存在这个属性,那么就回去它的__proto__属性所指向的对象(父类对象)上查找,如果父类对象依旧不存在这个属性,那么就回去其父类的__proto__属性所指向的父类的父类上去查找。以此类推,知道找到 null。而这个查找的过程,也就构成了我们常说的原型链。
prototype
在规范里,prototype 被定义为:给其它对象提供共享属性的对象。prototype 自己也是对象,只是被用以承担某个职能罢了. 所有对象,都可以作为另一个对象的 prototype 来用。
prototype是函数所独有的。!!它的作用就是包含可以给特定类型的所有实例提供共享的属性和方法。它的含义就是函数的远行对象,!!也就是这个函数所创建的实例的远行对象,正如上图:nealyang.__proto__ === Person.prototype。 任何函数在创建的时候,都会默认给该函数添加 prototype 属性.
constructor
constructor属性也是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数。 构造函数.prototype.constructor === 该构造函数本身
注意,每一个对象都有其对应的构造函数,本身或者继承而来。单从constructor这个属性来讲,只有prototype对象才有。每个函数在创建的时候,JavaScript 会同时创建一个该函数对应的prototype对象,而函数创建的对象.__proto__ === 该函数.prototype,该函数.prototype.constructor===该函数本身,故通过函数创建的对象即使自己没有constructor属性,它也能通过__proto__找到对应的constructor,所以任何对象最终都可以找到其对应的构造函数。
唯一特殊的可能就是我开篇抛出来的一个问题。JavaScript 原型的老祖宗:Function。它是它自己的构造函数。所以Function.prototype === Function.__proto__。
特殊的Object、Function
console.log(Function.prototype === Function.__proto__); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
typeof
基本用法
typeof 的用法想必大家都比较熟悉,一般被用于来判断一个变量的类型。我们可以使用 typeof 来判断number、undefined、symbol、string、function、boolean、object 这七种数据类型。但是遗憾的是,typeof 在判断 object 类型时候,有些许的尴尬。它并不能明确的告诉你,该 object 属于哪一种 object。
let s = new String('abc');
typeof s === 'object'// true
typeof null;//"object"
原理浅析
要想弄明白为什么 typeof 判断 null 为 object, 其实需要从js 底层如何存储变量类型来说其。虽然说,这是 JavaScript 设计的一个 bug。 在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。曾有一个 ECMAScript 的修复提案(通过选择性加入的方式),但被拒绝了。该提案会导致 typeof null === 'null'。 js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息:
1:整数
110:布尔
100:字符串
010:浮点数
000:对象
但是,对于 undefined 和 null 来说,这两个值的信息存储是有点特殊的:
null:所有机器码均为0
undefined:用 −2^30 整数来表示
所以在用 typeof 来判断变量类型的时候,我们需要注意,最好是用 typeof 来判断基本数据类型(包括symbol),避免对 null 的判断。
typeof 只是咱在讨论原型带出的 instanceof 的附加讨论区
instanceof
object
instanceof
constructor
instanceof 和 typeof 非常的类似。instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。与 typeof 方法不同的是,instanceof 方法要求开发者明确地确认对象为某特定类型。 基本用法
// 定义构造函数
function C(){}
function D(){}
var o = new C();
o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上
o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上
C.prototype = {};
var o2 = new C();
o2 instanceof C; // true
o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上.
D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上
如上,是 instanceof 的基本用法,它可以判断一个实例是否是其父类型或者祖先类型的实例。
console.log(Object instanceof Object);//true
console.log(Function instanceof Function);//true
console.log(Number instanceof Number);//false
console.log(String instanceof String);//false
console.log(Function instanceof Object);//true
console.log(Foo instanceof Function);//true
console.log(Foo instanceof Foo);//false
为什么 Object 和 Function instanceof 自己等于 true,而其他类 instanceof 自己却又不等于 true 呢?如何解释? 要想从根本上了解 instanceof 的奥秘,需要从两个方面着手:1,语言规范中是如何定义这个运算符的。2,JavaScript 原型继承机制。
原理浅析
经过上述的分析,相比大家对这种经典神图已经不那么陌生了吧,那咱就对着这张图来聊聊 instanceof
这里,我直接将规范定义翻译为 JavaScript 代码如下:
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显示原型
L = L.__proto__;// 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}
所以如上原理,加上上文解释的原型相关知识,我们再来解析下为什么Object 和 Function instanceof 自己等于 true。
Object instanceof Object
// 为了方便表述,首先区分左侧表达式和右侧表达式
ObjectL = Object, ObjectR = Object;
// 下面根据规范逐步推演
O = ObjectR.prototype = Object.prototype
L = ObjectL.__proto__ = Function.prototype
// 第一次判断
O != L
// 循环查找 L 是否还有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判断
O == L
// 返回 true
Function instanceof Function
// 为了方便表述,首先区分左侧表达式和右侧表达式
FunctionL = Function, FunctionR = Function;
// 下面根据规范逐步推演
O = FunctionR.prototype = Function.prototype
L = FunctionL.__proto__ = Function.prototype
// 第一次判断
O == L
// 返回 true
Foo instanceof Foo
// 为了方便表述,首先区分左侧表达式和右侧表达式
FooL = Foo, FooR = Foo;
// 下面根据规范逐步推演
O = FooR.prototype = Foo.prototype
L = FooL.__proto__ = Function.prototype
// 第一次判断
O != L
// 循环再次查找 L 是否还有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判断
O != L
// 再次循环查找 L 是否还有 __proto__
L = Object.prototype.__proto__ = null
// 第三次判断
L == null
// 返回 false
new 关键字
先从几个例子来看 new操作,做了什么
例子1
// 例子1
function Student(){
}
var student = new Student();
console.log(student); // {}
// student 是一个对象。
console.log(Object.prototype.toString.call(student)); // [object Object]
// 我们知道平时声明对象也可以用new Object(); 只是看起来更复杂
// 顺便提一下 `new Object`(不推荐)和Object()也是一样的效果
// 可以猜测内部做了一次判断,用new调用
/** if (!(this instanceof Object)) {
* return new Object();
* }
*/
var obj = new Object();
console.log(obj) // {}
console.log(Object.prototype.toString.call(student)); // [object Object]
typeof Student === 'function' // true
typeof Object === 'function' // true
从这里例子中,我们可以看出:一个函数用new操作符来调用后,生成了一个全新的对象。而且Student和Object都是函数,只不过Student是我们自定义的,Object是JS本身就内置的。 再来看下控制台输出图,感兴趣的读者可以在控制台试试。
与new Object() 生成的对象不同的是new Student()生成的对象中间还嵌套了一层__proto__,它的constructor是Student这个函数。 // 也就是说: student.constructor === Student; Student.prototype.constructor === Student;
- 从例子1 来看 new操作符做了两件事: 创建了一个全新的对象。 这个对象会被执行[[Prototype]](也就是__proto__)链接。
例子2
// 例子2
function Student(name){
console.log('赋值前-this', this); // {}
this.name = name;
console.log('赋值后-this', this); // {name: '若川'}
}
var student = new Student('若川');
console.log(student); // {name: '若川'}
由此可以看出:这里Student函数中的this指向new Student()生成的对象student。
- 小结2:从这个例子来看,new操作符又做了一件事:生成的新对象会绑定到函数调用的this。
例子3
// 例子3
function Student(name){
this.name = name;
// this.doSth();
}
Student.prototype.doSth = function() {
console.log(this.name);
};
var student1 = new Student('若');
var student2 = new Student('川');
console.log(student1, student1.doSth()); // {name: '若'} '若'
console.log(student2, student2.doSth()); // {name: '川'} '川'
student1.__proto__ === Student.prototype; // true
student2.__proto__ === Student.prototype; // true
// __proto__ 是浏览器实现的查看原型方案。
// 用ES5 则是:
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true
这个例子3再一次验证了小结1中的第2点。也就是这个对象会被执行[[Prototype]](也就是__proto__)链接。并且通过new Student()创建的每个对象将最终被[[Prototype]]链接到这个Student.protytype对象上。
例子4
// 例子4
function Student(name){
this.name = name;
// Null(空) null
// Undefined(未定义) undefined
// Number(数字) 1
// String(字符串)'1'
// Boolean(布尔) true
// Symbol(符号)(第六版新增) symbol
// Object(对象) {}
// Function(函数) function(){}
// Array(数组) []
// Date(日期) new Date()
// RegExp(正则表达式)/a/
// Error (错误) new Error()
// return /a/;
}
var student = new Student('若川');
console.log(student); {name: '若川'}
测试这七种类型后 JavaScript类型,得出的结果是:前面六种基本类型都会正常返回{name: '若川'},后面的Object(包含Functoin, Array, Date, RegExg, Error)都会直接返回这些值。
- 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
所以new操作做了这些事情
- 创建了一个全新的对象。
- 这个对象会被执行[[Prototype]](也就是__proto__)链接。
- 生成的新对象会绑定到函数调用的this。
- 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
- 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
new 的实现
/**
* 模拟实现 new 操作符
* @param {Function} ctor [构造函数]
* @return {Object|Function|Regex|Date|Error} [返回结果]
*/
function newOperator(ctor){
if(typeof ctor !== 'function'){
throw 'newOperator function the first param must be a function';
}
// ES6 new.target 是指向构造函数
newOperator.target = ctor;
// 1.创建一个全新的对象,
// 2.并且执行[[Prototype]]链接
// 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
var newObj = Object.create(ctor.prototype);
// ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments);
// 除去ctor构造函数的其余参数
var argsArr = [].slice.call(arguments, 1);
// 3.生成的新对象会绑定到函数调用的`this`。
// 获取到ctor函数返回结果
var ctorReturnResult = ctor.apply(newObj, argsArr);
// 小结4 中这些类型中合并起来只有Object和Function两种类型 typeof null 也是'object'所以要不等于null,排除null
var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
var isFunction = typeof ctorReturnResult === 'function';
if(isObject || isFunction){
return ctorReturnResult;
}
// 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表达式中的函数调用会自动返回这个新的对象。
return newObj;
}
Object.create()
经常会有这样的疑问?Object.create()到底做了什么工作? 像这样两行代码有什么不同?
var obj ={a: 1}
var b = obj
var c = Object.create(obj)
我们来做一点事情,
var obj ={a: 1}
var b = obj
console.log(obj.a) // 1
console.log(b.a) // 1
b.a = 2
console.log(obj.a) //2
var obj ={a: 1}
var b = Object.create(obj)
console.log(obj.a) // 1
console.log(b.a) // 1
b.a = 2
console.log(obj.a) //1
所以我们立马可以想到Object.create貌似创建了一个新的对象,这个对象继承(关联)了obj的属性,改变新对象的同名属性并不会影响原对象。 如果直接用“=”来赋值,只是一个对象的引用。
那么,为什么会这样呢?是因为Object.create()复制了一个新对象么?实际上并不是,只是Object.create()返回了一个新的空对象,并且这个空对象的构造函数的原型(prototype)是指向obj的。所以当我们访问新对象b.a的时候实际上是通过原型链访问的obj中的a。 当我们试图修改b.a的时候,这里有一个知识点(对象的遮蔽效应,如果修改对象的一个与原型链同名属性,那么会在当前对象中新建一个改属性,这个属性拥有更高级的访问优先级,所以就会遮蔽原型链中的同名属性) 所以Object.create的具体内部实现模拟
_create = function (o) {
let F = function () {}
F.prototype = o
return new F()
}
再来看这个例子
var person = {
friends : ["Van","Louis","Nick"]
};
var anotherPerson = _create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = _create(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"
这里是使用Object.create
var person = {
friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);
相当于做了一次浅复制,新创建的各个对象实际上是会共享原始对象中的引用类型的值,这意味着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享 实际上真正的Object.create()还可以传入第二个参数,这个参数与Object.defineProperties方法的第二个参数格式相同, 通过第二个参数是会在新对象中重新创建一个属性的,然后通过属性遮蔽原理避免修改原对象。
var person = {
name : "Van"
};
var anotherPerson = Object.create(person, {
name : {
value : "Louis"
}
});
alert(anotherPerson.name);//"Louis"
Object.create(null) 会创建一个真正的空对象,并没有继承Object原型链上的方法
var a = {} 这并不是一个纯粹的空对象,它会继承原型链上的很多方法
js继承
ES5 继承
类式继承/原型继承
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)
// {
// "books": [
// "js",
// "css",
// "html"
// ],
// "id": 10
// }
console.log(instance2)
// {
// "books": [
// "js",
// "css"
// ],
// "id": 11
// }
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.来添加,否则直接赋予对象就会覆盖从父类原型继承的对象了.
Object.__proto__
Object.prototype
Object.prototype.__proto__
Object.prototype.__proto__.constructor // 没有这个 因为 Object.prototype.__proto__ 是null
Object.prototype.constructor
Function.__proto__
Function.prototype
Functioin.prototype.__porto__
Functioin.prototype.__porto__.constructor
Function.prototype.contructor