1.setTimeout()
js中规定,定时器的第二个参数设置最少不能小于4ms, 小于的话就按最小的4ms执行
2.==和===区别
==:
类型不同则转换类型继续比较
类型相同则比较值是否相同
undefined和null相等
===:
严格相等,先比较值类型是否相同,不同则不等
undefined和null不相等
3.Array.reduce
4.this指向问题,new function和为什么不能new箭头函数
this指向是在函数执行时才能确定的,this指向最终调用它的对象
var obj = {
name: "john",
b: {
name: "jenny",
say() {
console.log(this.name);
console.log(this);
}
}
}
obj.b.say();
this.name输出jenny,this指向b。这是由于最终调用say函数的即为obj下的b对象
var obj = {
name: "john",
b: {
name: "jenny",
say() {
console.log(this.name);
console.log(this);
}
}
}
var f = obj.b.say;
f();
this.name输出undefined,this指向window;这是因为将say函数赋值给了f,而最终调用f的为window对象
构造函数版this,即使用new关键字实例化对象时this指向问题
var f = function(name) {
this.name = name;
console.log(this.name); //john
console.log(this); //f{name: john}
}
var obj = new f("john");
可见,此时this指向实例obj,这是因为new关键字执行了以下几步:
var obj = {}; //创建一个空对象
obj.__proto__ = f.prototype; //将对象的原型链指向f的原型
f.call(obj, ...args) //绑定obj为this指向
var f = function(name) {
this.name = name;
console.log(this.name);
console.log(this);
}
var obj = new f("john"); //this.name=john, this指向obj
var obj1 = {};
obj1.__proto__ = f.prototype;
f.call(obj1, "john"); //this.name=john, this指向obj1
console.log(JSON.stringify(obj) === JSON.stringify(obj1)); //true
由此可见new构造函数是需要用到this的,而箭头函数没有自己的this(也没有arguments),它的this取决于包裹该箭头函数的第一个普通函数,因此箭头函数不能使用new关键字
var f= (...args) => {
this.name = name;
this.age = age;
console.log(this);
}
var obj = new f('john', 18) //Uncaught TypeError: f is not a constructor
var name = "jenny";
var obj = {
name: "john",
say: function() {
return function() {
return () => {
console.log(this.name); //jenny
console.log(this); //window
}
}
}
}
obj.say()()()
此时包含箭头函数的第一个普通函数为包含它的外层匿名函数,此时匿名函数this指向window,因此箭头函数this也指向window
var name = "jenny";
var obj = {
name: "john",
say: function() {
return () => {
return () => {
console.log(this.name); //john
console.log(this); //obj
}
}
}
}
obj.say()()()
此时包含箭头函数的第一个普通函数为say函数,此时say函数由obj调用,this指向obj,因此箭头函数this也指向obj
当new实例化实例时,如果有返回值,且返回值为对象或Function,则得到的为返回的对象或Function,其它返回值则得到的为实例本身,返回值为null时得到的也为实例本身
function f(name) {
this.name = name;
// return { name: "jenny" } //此时obj为{ name: "jenny" }
// return function() {} //此时obj为此匿名函数
// return undefined //此时obj为f { name: 'john' }
// return "jenny" //此时obj为f { name: 'john' }
// return true //此时obj为f { name: 'john' }
// return null //此时obj为f { name: 'john' }
}
var obj = new f("john");
console.log(obj)
关于bind与new的问题
当bind遇到new时,不起作用
var value = 2;
var foo = { value: 1 };
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18'); // undefined // daisy // 18
关于bind函数的this指向问题
无论函数绑定多少次,this都指向第一个绑定的对象
var obj1 = {
name: "john"
}
var obj2 = {
name: "jenny"
}
function f() {
console.log(this.name); //john
console.log(this); //obj1
}
f.bind(obj1).bind(obj2)();
bind是如下实现的:
//第一种方法
Function.prototype.mybind = function(context) {
var _this = this;
return function(){
_this.apply(context, arguments)
}
}
//第二种方法
Function.prototype.my_bind = function() {
let _this = this;
let context = arguments[0];
let args = Array.prototype.slice.call(arguments, 1);
return function() {
_this.apply(context, args.concat(Array.prototype.slice.call(arguments)));
}
}
第一种方法绑定时无法传参,第二种方法绑定时可传参
当f被绑定两次时其实执行过程如下所示:
f1 = function() {
f.apply(obj1, arguments);
}
f2 = function() {
f1.apply(obj2, arguments);
}
f2()
由上可见,当绑定一次后,后续无论绑定几次都是无用的,因为f1中根本没有this,再执行apply是徒劳的
严格模式下this指向
全局作用域的this无论是否为严格模式,都指向window
全局作用域下函数的this指向,非严格模式指向window,严格模式指向undefined
5.类数组转化为数组
如function中的arguments:
var f = function(){
let arg1 = [...arguments];
let arg2 = Array.from(arguments);
let arg3 = Array.prototype.slice.call(arguments);
console.log(arg1); //[1, 2]
console.log(arg2); //[1, 2]
console.log(arg3); //[1, 2]
}
f(1, 2);
6.Object.property.toString和Object.property.valueOf
valueOf函数对于对象和数组会输出原值,不会返回类型,对于undefined和null会报错
toString会返回所有数据类型,null返回NULL,undefined返回UNDEFINED
另外,通过原型链Object.property.toString才可以判断数据类型,当直接通过变量调用时,除null,undefined会报错,对象仍然输出object Object外,其他均转换成字符串输出。这是因为其他数据类型在原型链上重写了toString方法,当通过原型链一步步向上查找时会查找到这些toString方法就停止
一个空对象{}参与==比较时,首先通过toString方法转换为[object Object],如果需要继续转换,则通过Number转化为NaN
一个空数组[]参与==比较时,首先通过toString方法转换为空字符串"",如果需要继续转换,则通过Number转化为0
7.if判断成立条件
当判断内容不为undefined,null,"" ,false,NaN时
8.event-loop
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout")
}, 0);
async1();
new Promise((resolve) => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2");
});
console.log("script end");
/*
输出: script start/ async1 start/ async2/ promise1/ script end/ async1 end/ promise2/ setTimeout
Microtask: [await async2() .then]
Macrotask: [setTimeout]
执行过程:主线程 -> 清空微任务队列 -> 取出一个宏任务 -> 清空微任务队列 -> 取出一个宏任务
*/
首先,寻找同步函数,找到console.log("script start"),则输出“script start”;
然后发现setTimeout函数,放入宏任务(Macrotask)队列中;
接着执行async1函数,console.log("async1 start");为同步执行,则输出async1 start;
继续调用async2,也是同步执行,输出async2;await后的代码相当于Promise.then,放入微任务队列中;
继续执行主线程同步函数,发现Promise,new Promise中函数为同步执行,则输出promise1;发现.then,放入微任务队列中;
继续执行主线程,输出script end;此时主线程执行完毕,先清空微任务队列,则先输出async1 end,然后输出promise2;
清空微任务队列后,取出一个宏任务执行,输出setTimeout,执行完毕
注意:微任务执行过程中若再遇到微任务,则将其放到微任务队列末尾,仍先于宏任务执行,当微任务队列清空后再去执行宏任务;例如在async1函数中,console.log("async1 end")后添加await async2();console.log("async1 end 2"),会被放到微任务队列末尾,但async1 end 2仍先于setTimeout输出。
console.log("script start");
setTimeout(() => {
console.log("setTimeout1");
Promise.resolve()
.then(() => { console.log("promise1") })
.then(() => { console.log("promise2") })
console.log("setTimeout1 end")
}, 0);
Promise.resolve().then(() => {
console.log("promise3");
}).then(() => {
console.log("promise4");
});
setTimeout(() => {
console.log("setTimeout2");
}, 0);
console.log("script end");
//输出: script start/ script end/ promise3/ promise4/ setTimeout1/ setTimeout1 end/ promise1/ promise2/ setTimeout2
首先执行主线程同步代码,输出script start
遇到setTimeout放入宏任务队列
遇到Promise.resolve().then().then(),将两个.then放入微任务队列
再次遇到setTimeout,放入宏任务队列
执行同步代码,输出script end
同步代码执行完毕,清空微任务队列,分别输出promise3,promise4
取出一个宏任务执行,即第一个setTimeout,首先输出setTimeout1,遇到Promise,将两个.then放入微任务队列,继续执行第一个setTimeout同步代码,输出setTimeout1 end
第一个宏任务setTimeout执行完毕,清空微任务队列,输出promise1,promise2
继续取出一个宏任务执行,即第二个setTimeout,输出setTimeout2
setTimeout(() => {
console.log("setTimeout1");
Promise.resolve()
.then(() => { console.log("promise1") })
.then(() => { console.log("promise2") })
console.log("setTimeout1 end")
}, 10);
setTimeout(() => {
console.log("setTimeout2");
}, 0);
//输出: setTimeout2\ setTimeout1\ setTimeout1 end\ promise1\ promise2
由于第一个setTimeout延迟10ms,第二个setTimeout延迟0ms,因此第二个setTimeout先放入宏任务队列,首先输出setTimeout2,其余执行顺序与先前相同
9.async await和Promise
async函数返回值为Promise对象,返回值可以使用.then添加回调函数。也可使用await获取返回Promise对象的值,感觉类似.then。async函数中发生错误时,可以使用
return Promise.reject(err)
将错误返回,在接收返回值时,可以利用f().then().catch(e)获取到错误,也可以在另一个async函数中利用try{}catch(e){}获取到错误。
new Promise对象无确定返回值时,会返回一个Promise对象,值为<pending>,即Promise {<pending>}.
Promise.all当所有Promise均返回的时候执行.then或.catch回调函数,当所有Promise均成功resolve返回值的时候执行.then,返回结果为所有返回值的一个数组,并且所有值按传入参数顺序排列;当有一个发生reject时,执行.catch,并返回第一个发生错误的值。
Promise.race返回第一个执行完毕的值,当成功时执行.then,失败时执行.catch
10.let与var
var定义全局变量;let定义局部变量,只在局部作用域内可以被访问。
{
var a = 10;
}
console.log(a); //10
{
let a = 10;
}
console.log(a); //ReferenceError: a is not defined
var存在变量提升,可以在声明前使用,此时变量为undefined;
let不存在变量提升,不能在声明前使用,否则会报错。
var能够重复声明同一个变量,let不允许同一作用域内重复声明变量。
for(var i=0; i<5; i++) {
setTimeout(() => {
console.log(i);
}, 0)
} // 输出5,5,5,5,5
for(let j=0; j<5; j++) {
setTimeout(() => {
console.log(j);
}, 0)
} // 输出0,1,2,3,4
11.变量提升
变量提升即变量的声明被提前到使用时前面
console.log(a);
var a = 100;
function f() {
console.log(a);
var a = 200;
console.log(a);
}
f();
console.log(a);
// 输出:undefined undefined 200 100
这是由于变量提升被编译成如下形式:
var a;
console.log(a); //此时a为undefined
a = 100;
function f() {
var a;
console.log(a); //此时a为undefined
a = 200;
console.log(a); //此时a为200
}
f();
console.log(a); //此时a为100
函数的变量提升:
函数有两种声明方式:变量式声明和函数式声明,变量式声明函数的变量提升与普通变量提升一样,只将变量声明提前到前边,不会将内容一同提升。函数式声明函数的变量提升将函数的声明与函数内容一同提前到作用域最前面。
f1();
function f1() {
console.log('f1');
} //输出f1
f2();
var f2 = function() {
console.log('f2');
} //Typeerror: f2 is not a function
12.prototype, __proto__, constructor,原型链,继承
对象具有__proto__和constructor属性
函数具有__proto__,constructor,prototype属性,函数的prototype是一个对象,也具有__proto__属性
对象的__proto__指向它的构造函数的prototype,所有原型链的最终都指向Object.prototype,而Object.prototype的__proto__指向null
所有函数自身的__proto__均指向Function.prototype,因为他们都是Function的实例
对象的constructor指向它的构造函数,函数prototype的constructor指向本身。所有constructor链最终指向Function,而Function的constructor指向本身
原型链继承
函数的原型链继承是通过函数的prototype上的__proto__指向实现的。例如父类构造函数和子类构造函数的原型链继承,就是通过令子类构造函数的prototype的__proto__指向父类构造函数的prototype实现的。实例的__proto__指向它的构造函数,因此只需令子类构造函数的prototype等于父类构造函数的一个实例即可,也就是说令子构造函数的prototype相当于一个父类构造函数的实例即可。实现方式如下:
var f1 = function(name, age) {
this.name = name;
this.age = age;
}
f1.prototype.say = function() {
console.log("name is " + this.name);
}
var f2 = function(fav) {
this.fav = fav;
}
var obj = new f1("john", 18);
f2.prototype = obj;
// f2.prototype = Object.create(f1.prototype) //相当于创建一个__proto__指向f1.prototype的空对象
f2.prototype.constructor = f2;
var obj1 = new f2("web");
var obj2 = new f2("book");
obj1.say(); //name is john
obj2.say(); //name is john
console.log(obj1.__proto__); //f2 { name: 'john', age: 18, constructor: [Function: f2] }
console.log(obj2.__proto__); //f2 { name: 'john', age: 18, constructor: [Function: f2] }
如上所示,原型链继承可继承父类的所有属性和方法,但将私有属性转换为了公有属性
寄生继承
寄生继承指的是通过在子类构造函数利用call改变this指向并调用父类构造函数,将父类属性继承到子类
var f1 = function(name, age) {
this.name = name;
this.age = age;
}
f1.prototype.say = function() {
console.log("name is " + this.name);
}
var f2 = function(fav) {
f1.call(this, "john", 18); //将this指向f2实例,并调用f1赋值,从而继承f1私有属性
this.fav = fav;
}
var obj = new f2("web");
console.log(obj); //f2 { name: 'john', age: 18, fav: 'web' }
obj.say(); //TypeError: obj.say is not a function
寄生继承可以继承父类私有属性,但无法继承父类公有方法
组合寄生继承
将原型链继承和寄生继承结合起来,这样既可继承到父类的私有属性,又可继承到父类的公有方法
var f1 = function(name, age) {
this.name = name;
this.age = age;
}
f1.prototype.say = function() {
console.log("name is " + this.name);
}
var f2 = function(fav) {
f1.call(this, "john", 18);
this.fav = fav;
}
f2.prototype = Object.create(f1.prototype)
f2.prototype.constructor = f2;
var obj = new f2("web");
console.log(obj); //f2 { name: 'john', age: 18, fav: 'web' }
obj.say(); //name is john
组合寄生继承既可继承到父类的私有属性,且在子类仍为私有属性,又可继承到父类的公有方法
ES6继承
ES6通过class和extends来完成类的继承:
class A {
constructor(x) {
this.x = x;
}
getX() {
console.log(this.x)
}
}
var a = new A(100);
class B extends A {
constructor(y) {
super(200);
this.y = y;
}
getY() {
console.log(this.y);
}
}
var b = new B(300);
console.log(a); //A { x: 100 }
console.log(b); //B { x: 200, y: 300 }
a.getX(); //100
b.getX(); //200
b.getY(); //300
console.log(a.getX === b.getX); //true
子类可继承到父类的所有属性和方法,并且私有属性仍为子类的私有属性,方法为所有子类公有方法。
需要注意的是,子类constructor函数第一句必须为super()。若子类不写构造函数,则自动生成一个构造函数:
constructor() {
super()
}
并且当子类没有构造函数而实例化传值时,默认为给父类传值:
class A {
constructor(x) {
this.x = x;
}
getX() {
console.log(this.x)
}
}
var a = new A(100);
class B extends A {
getY() {
console.log(this.y);
}
}
var b = new B(300);
console.log(a); //A { x: 100 }
console.log(b); //B { x: 300 }
13.作用域,作用域链,闭包
在ES6之前,作用域仅分为全局作用域和函数作用域
ES6新增块级作用域,可使用let或const定义块级作用域变量
ES6之前只有在function中会形成函数作用域,外部不可访问,此外无论是大括号内部{},还是if,switch,for循环内部,都不会创建新的作用域
{
var a = 10;
}
console.log(a); //10
if(true) {
var b = 20;
}
console.log(b); //20
var c = 0;
while(c<5) {
c++;
var d = 30;
}
console.log(d); //30
function demo() {
var e = 40;
}
console.log(e); //ReferenceError: e is not defined
作用域是分层的,内层作用域可访问外层作用域变量,反之则不行
var a = 10;
function demo() {
var b = 20;
console.log(a);
}
demo(); //10
console.log(b); //ReferenceError: b is not defined
当函数作用域变量未经声明,直接赋值时,其被作为全局变量。但外部访问时需先执行此函数,获取此变量的值,否则仍无法访问
function demo() {
a = 10;
var b = 20;
}
demo();
console.log(a); //10
console.log(b); //ReferenceError: b is not defined
function demo() {
a = 10;
var b = 20;
}
//demo(); //不执行demo函数
console.log(a); ReferenceError: a is not defined
console.log(b);
作用域链
自有变量:当前作用域没有的变量,称为自有变量。
自由变量到函数定义时的父级作用域寻找,若父级作用域没有则再向外层寻找,一层一层由内向外寻找,这就是作用域链。
变量的取值为就近原则,当内部作用域有变量定义时,则不再向外部寻找
var a = 10;
function demo() {
console.log(a);
var a;
}
demo(); //undefined 变量提升
var a = 10;
function demo() {
console.log(a);
function inner() {
var a = 20;
}
}
demo(); //10,由内向外查找变量,var a = 20属于更内部
与执行上下文在执行时才确定(典型的this指向问题)不同,作用域是在定义时就确定的,即寻找自由变量的值时,由定义时的作用域查找,而非执行时的作用域查找:
var a = 10;
function fn() {
console.log(a);
}
function show(f) {
var a = 20;
(function() {
f()
})()
}
show(fn); //10
show(fn),传入的函数参数为fn,内部f()执行,即执行fn,由fn定义时的作用域而非执行时的作用域查找a变量的值,查找到为10,因此输出10而非20
var a = 10;
function fn() {
var b = 20;
return function() {
console.log(a+b);
}
}
var f = fn(); //f即为fn返回的匿名函数
var b = 200;
f(); //30 匿名函数执行,在匿名函数定义时作用域查找变量值
var f = fn()得到的是一个匿名函数,f()即执行匿名函数,此时作用域为匿名函数定义时的作用域,查找到a,b的值分别为10,20因此输出30
闭包
简单定义:在函数内部调用外部变量就会形成闭包
另一种定义:当函数执行完毕后,函数执行上下文内某些内容被执行上下文以外的内容引用,使得当前函数执行上下文不能被释放
所有函数执行时都会形成自己的私有上下文,私有上下文中会存放自己的私有变量
函数声明时会存储在一个堆内存中,堆内存中存放三部分内容:
作用域:当前函数所在作用域,或者说所在执行上下文
形参
代码块字符串
函数执行分为三步:
1.创建作用域链:<私有上下文,声明时所在作用域>
2.形参赋值:给当前形参进行赋值,并存入自己的私有上下文中
3.代码执行
执行上下文销毁:
function out() {
var name = "john";
function inner() {
console.log(name);
}
inner();
}
out(); //执行完毕后,out执行上下文被销毁
function out() {
var name = "john";
return function() {
console.log(name);
}
}
out()(); //执行完毕后,out执行上下文被销毁
function out() {
var name = "john";
return function() {
console.log(name);
}
}
var f = out();
f(); //out返回的匿名函数被全局变量f引用,导致out执行上下文不会被销毁
f = null; // f指向null,匿名函数不再被上下文外的变量引用,out执行上下文被销毁
闭包应用:
function getArr() {
var res = [];
for(var i=0; i<10; i++) {
res[i] = function() {
console.log("i = " + i);
}
}
return res;
}
var arr = getArr();
for(var i=0; i<10; i++) {
arr[i]();
}
//输出10个10
所有res[i]函数均在getArr第一次调用时形成的同一个执行上下文生成,因此它们声明时的作用域均为EC(getArr1),而由于arr引用了getArr的返回值,因此EC(getArr1)不会被释放。
由于变量提升,i被重复定义,最终EC(getArr1)内i=10。
因此当每个arr[i]被调用时,由于arr[i]本身执行上下文内没有变量i的取值,因此均向EC(getArr1)中查找,查找到i=10,因此均输出为10。
改变:
function getArr() {
var res = [];
for(var i=0; i<10; i++) {
res[i] = (function(j){ //利用立即执行函数形成不同的执行上下文
return function() {
console.log("i = " + j);
}
})(i)
}
return res;
}
var arr = getArr();
for(var i=0; i<10; i++) {
arr[i]();
}
//输出0,1,2,3,4,5,6,7,8,9
与上面不同的是,下面利用了一个立即执行函数来生成不同的执行上下文,在立即执行函数的执行上下文中保存了不同的变量j。
此时返回的匿名函数的作用域为不同的立即执行函数,且由于被外部res[i]引用返回的匿名函数,因此立即执行函数的执行上下文不会被销毁。
当每个arr[i]被调用时,由于arr[i]本身执行上下文内没有变量j的取值,因此向外部立即执行函数的执行上下文中去寻找,分别寻找到j=0,1,...,9,因此输出分别为i=0,1,...,9。
14. ... 运算符
方法调用时传值,将数组扩展成参数:
function sum(a, b, c) {
console.log(a + b + c); //6
}
var arr = [1, 2, 3];
sum(...arr); //...arr返回1 2 3,并作为参数传入sum函数
数组与对象的浅拷贝:
var arr1 = [1, 2, 3];
var arr2 = [...arr1];
arr2[0] = 4;
console.log(arr1, arr2); //[ 1, 2, 3 ] [ 4, 2, 3 ]
var obj1 = { name: "john", age: 18 };
var obj2 = { ...obj1 };
obj2.name = "jenny";
console.log(obj1, obj2); //{ name: 'john', age: 18 } { name: 'jenny', age: 18 }
var obj3 = { fav: "web", info: {name: "john", age: 18} };
var obj4 = { ...obj3 };
obj4.info.name = "jenny";
console.log(obj3, obj4); //{ fav: 'web', info: { name: 'jenny', age: 18 } } { fav: 'web', info: { name: 'jenny', age: 18 } }
如上所示,与assign类似,都是浅拷贝,只拷贝第一层
字符串转为数组:
var str = "john";
var arr = [...str];
console.log(arr); //[ 'j', 'o', 'h', 'n' ]
数组拼接:
var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
var arr3 = [...arr1, ...arr2];
console.log(arr3); //[ 1, 2, 3, 4, 5, 6 ]
对象拼接:
var obj1 = { age: 18 };
var obj2 = { name: "john", ...obj1 };
console.log(obj2); //{ name: 'john', age: 18 }
当参数个数不确定时,剩余参数:
function sum(...args) {
console.log(args); //[1, 2, 3]
}
sum(1, 2, 3);
function sum(a, ...args) {
console.log(a); //1
console.log(args); //[2, 3]
}
sum(1, 2, 3);
解构时配合使用:
var arr1 = [1, 2, 3, 4, 5];
var [a, b, ...rest] = arr1;
console.log(a, b);
console.log(rest);
var obj = {
name: "john",
age: 18,
fav: "web"
}
var { name, ...rest } = obj;
console.log(name); //john
console.log(rest); //{ age: 18, fav: 'web' }
解构函数形参:
function f(...[a, b, c]) {
console.log(a);
console.log(b);
console.log(c);
}
f(1); //1, undefined, undefined
f(1, 2); //1, 2, undefined
f(1, 2, 3); //1, 2, 3
15.函数式编程
高阶函数,函数柯里化,记忆函数
高阶函数:函数形参或返回值
高阶函数可以对一个函数进行二次包装,添加一些功能,而不影响原函数
// 例如我们想在一个函数执行前执行一些代码
// 但又不想改变该函数的原代码,以免影响其他地方的调用
// 这时候可以利用高级函数对此函数进行包装,实现此功能
function say(...args) {
console.log("say");
console.log(args);
}
function before(beforeFn, fn) {
return (...args) => {
beforeFn();
fn(...args);
}
}
var newSay = before(() => {
console.log("before say");
}, say)
say(1, 2, 3); //say, [1, 2, 3]
newSay(1, 2, 3); //before say, say, [1, 2, 3]
函数柯里化:使得多个参数可以分批传入
const cursing = (fn, ...args) => {
let argLen = fn.length;
return (...argsNew) => {
let argsAll = [...args, ...argsNew];
if(argsAll.length < argLen) {
return cursing(fn, ...argsAll);
} else {
return fn(...argsAll);
}
}
}
function sum(a, b, c, d, e) {
console.log(a + b + c + d + e);
}
let newSum = cursing(sum, 1, 2);
newSum(3)(4)(5);
记忆函数:利用缓存去存储计算结果,当遇到相同计算时直接调取存储的结果,以空间换时间
16.webworker
17.Promise原理
实现基本的Promise
// 实现一个Promise基本类
// Promise有自己的同步函数excutor,且new Promise时直接执行
// excutor具有两个参数,resolve和reject函数,当调用resolve时将状态由pending转换为fulfilled,当调用reject时将状态由pending转换为rejected
// Promise有三种状态,pending, fulfilled, rejected;初始状态为pending,当变为fulfilled时调用resolve,当状态变为rejected时调用reject
// Promise状态改变只能由pending转换为fulfilled或rejected,且只能转换一次
// 当用户手动抛出一个错误时,也可以将状态转换为rejected
// Promise具有then函数,then函数接收两个函数(fulfilledFn, rejectFn)作为参数,分别对应fulfilled和rejected状态时执行
// fulfilledFn和rejectFn都接收一个参数,分别为成功时resolve的value和失败时reject的reason
/*
当Promise的excutor函数中遇到异步返回值时,会被放到异步队列中,继续执行then函数,此时状态为pending
若不进行任何处理,则当异步操作完成后由于then函数已执行完毕,相应回调也不会再执行
因此需要在then函数中status为PENDING时进行处理,若此时状态为PENDING,则先将所有回调函数存储起来,由于可能会多次调用then函数,因此存储的回调可能是个数组
等到异步操作完成后,依次调用这些回调进行处理
*/
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
class Promise {
constructor(excutor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolveCallbacks = [];
this.onRejectCallbacks = [];
let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolveCallbacks.forEach((cb) => {
cb();
})
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectCallbacks.forEach((cb) => {
cb();
})
}
}
try {
excutor(resolve, reject)
}catch(e) {
reject(e);
}
}
then(fulfilledFn, rejectFn) {
if(this.status === FULFILLED) {
fulfilledFn(this.value);
}
if(this.status === REJECTED) {
rejectFn(this.reason);
}
if(this.status === PENDING) {
this.onResolveCallbacks.push(() => {
fulfilledFn(this.value);
});
this.onRejectCallbacks.push(() => {
rejectFn(this.reason);
})
}
}
}
module.exports = Promise;
18.for...in和for...of
1.遍历数组时:for...in遍历的为数组的key,for...of遍历的为数组的值
var arr = ["a", "b", "c"];
for(let item in arr) {
console.log(item); //0, 1, 2
}
for(let item of arr) {
console.log(item); //a, b, c
}
2.遍历对象:只能用for...in,且遍历的为对象的属性;for...of只能遍历可迭代类型,因此直接遍历对象会出错,可配合Object.keys()使用
var obj = { name: "john", age: 18 };
console.time("for-in")
for( let key in obj) {
console.log("key: " + key);
console.log("value: " + obj[key]);
}
console.timeEnd("for-in"); //for-in: 3.799ms
console.time("for-of")
for( let key of Object.keys(obj)) {
console.log("key: " + key);
console.log("value: " + obj[key]);
}
console.timeEnd("for-of"); //for-of: 0.232ms
3.for...of比for...in要快很多,如上例所示
执行效率:for...of > forEach > for...in
19. async await
在async函数里面,可以先并行调用异步操作,在串行使用await等待结果,提高效率
异步串行await:
async function f1(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x*2);
}, 1000)
})
}
(async function() {
console.time("async")
let res1 = await f1(10);
let res2 = await f1(20);
console.log(res1);
console.log(res2);
console.timeEnd("async");
})()
//async: 2008.896ms
修改为并行调用后:
async function f1(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x*2);
}, 1000)
})
}
(async function() {
console.time("async")
let promise1 = f1(10);
let promise2 = f1(20);
let res1 = await promise1;
let res2 = await promise2;
console.log(res1);
console.log(res2);
console.timeEnd("async");
})()
//async: 1013.501ms
原理:
使用Generator函数,通过判断自身执行状态递归调用,利用Promise实现等待异步执行
function f1() {
return new Promise(resolve => {
setTimeout(() => {
resolve(100)
}, 5000);
})
}
function f2() {
return new Promise(resolve => {
setTimeout(() => {
resolve(200)
}, 1000);
})
}
function *g() {
let res1 = yield f1();
console.log(res1);
let res2 = yield f2();
console.log(res2);
let res3 = 300;
console.log(res3);
return f1();
}
function exec(gen) {
return new Promise((resolve, reject) => {
let it = gen();
let next = (result) => {
try {
let res = it.next(result);
if(!res.done) {
res.value.then((data) => {
next(data);
})
} else {
resolve(res.value);
}
}catch(e) {
reject(e);
}
}
next();
})
}
exec(g).then((data) => {
console.log("exec end: " + data);
})
当利用async和await时,相当于将声明的async函数改变为类似*g的Generator函数,await相当于yield,然后利用一个自动执行函数去执行Generator函数,最终返回一个Promise对象。
在自动执行函数内部,每执行一步就去判断是否执行完毕,若未执行完毕则递归调用,由于返回值是Promise对象,因此需要在.then内去调用。
Generator函数每步执行next()可传递参数,并将传递的参数作为上一步执行的结果,这样在每一步执行完毕后,将结果作为参数传递进去执行下一步,Generator函数内就可获得上一步执行的结果
20.JS创建指定大小二维数组,并全部以0填充
//创建4行3列且元素全部为0的数组
let arr = Array(4).fill(0).map(item => Array(3).fill(0))
21. typeof可以返回的类型
symbol, number, string, function, object, boolean, object, undefined
22. 原生js设置id为button1的按钮禁用
document.getElementById("button1").setAttribute("Readonly", true);
document.getElementById("button1").disabled=true;
23.变量提升,this指向,new操作,原型链
function Foo() {
getName = function () {
log(1);
};
console.log(this);
return this;
}
Foo.getName = function () { //静态方法,不会添加到原型上
log(2);
};
Foo.prototype.getName = function () {
log(3);
};
var getName = function(){
log(4);
};
function getName(){
log(5);
}
Foo.getName(); //2
getName(); //4
Foo().getName(); //window, 1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //Foo {}, 3
new new Foo().getName(); //Foo {}, 3
解析:
第一行:Foo.getName()输出2
这是因为Foo本身具有getName属性,因此直接调用Foo的getName函数,输出2;
第二行:getName()输出4
这是因为function定义的getName会由于变量提升从而提升到最前,执行时候先执行,后执行过程中又被getName = function(){log(4);}函数覆盖,如下:
var getName;
function getName(){
log(5);
};
getName = function(){
log(4);
};
因此最终输出4;
第三行:Foo().getName()输出window对象和1
这是因为现在是window调用Foo函数,因此this指向window,输出的this即为window对象;
而且由于Foo函数内部getName函数没有加let声明,因此getName为全局变量,此时重定义了全局getName函数,覆盖掉了之前的函数getName = function(){log(4);},此时全局函数getName = function () {log(1);};
返回this即返回window对象;接下去相当于执行this(window).getName(),输出为1;
第四行:getName()输出为1
这是因为上一步执行Foo函数时候全局getName函数已被重定义为getName = function () {log(1);},因此输出为1;
第五行:new Foo.getName()输出为2
这是因为这相当于new了一个Foo.getName的实例对象,实例化过程中会调用Foo.getName函数,输出2;
第六行:new Foo().getName()输出为一个Foo类型的空对象和3,这是因为这相当于执行了以下两步:
var obj = new Foo();
obj.getName();
通过new关键字实例化对象时,会执行以下三步:
var obj = {};
obj.__proto__ = Foo.prototype;
Foo.call(obj, arguments);
若无返回值,则最终返回一个Foo的实例化对象,若有返回值,且返回值为对象(不包括null),则最终返回值为该对象;
此代码中先是new Foo()创建了一个指向Foo的实例空对象,然后将Foo的this指向该对象并调用Foo函数,因此输出的this为该实例空对象;
实例化完成后,返回一个this,此时this指向该实例空对象,因此返回的即为该实例空对象;
接下来调用该实例空对象的getName函数,该对象本身属性没有getName,因此去原型链寻找,寻找到Foo.prototype.getName = function () {log(3);},因此输出3;
第七行:new new Foo().getName()输出Foo {}和3;
相当于先new一个Foo对象实例,此过程中输出this,this指向实例对象,因此输出的为Foo {},然后返回该实例对象;
然后再new一个该空实例对象的getName函数的一个实例,getName为该空实例对象原型链上的getName函数,即Foo.prototype.getName = function () {log(3);},new一个该函数的实例,过程中输出3。
function Foo() {
this.getName = function () {
log(1);
};
console.log(this);
return this;
}
Foo.getName = function () { //静态方法,不会添加到原型上
log(2);
};
Foo.prototype.getName = function () {
log(3);
};
var getName = function(){
log(4);
};
function getName(){
log(5);
}
Foo.getName(); //2
getName(); //4
Foo().getName(); //window, 1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //Foo {getName: ƒ}, 1
new new Foo().getName(); //Foo {getName: ƒ}, 1
前五行输出大致与上述相同,六七行不同;
这是因为在Foo内部定义getName函数时前面添加了this,this.getName = function () {log(1);};
因此在实例化Foo函数时,会为每个实例添加一个私有属性getName,其属性值为函数,且每个实例的getName函数都是一个新的函数,互不干扰;
因此再调用Foo实例上的getName函数(直接调用或实例化调用)时,调用的即为该对象自身的getName函数,不再去原型链查找,因此输出为1。
函数调用时的变量寻找:
首先看有没有this,有this的话看this指向谁,然后去this指向的对象中去寻找该变量,若没有则去该对象原型链上去寻找,一直寻找到原型链结束。
若没有this,则去函数声明时作用域去寻找,若没有则去上层作用域寻找,直到寻找到顶层。
24.对象连续赋值及.符优先级
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a); // { n: 2 }
console.log(b); // { n: 1, x: { n: 2 } }
解释:a为引用类型,b=a,则此时b与a指向同一地址;
.符优先级要比=高,因此先执行a.x,由于此时a与b指向同一地址,因此对a,b同时添加x属性,即对这个地址存储的对象添加x属性;
而=赋值操作由右至左,因此先对a赋值,此时由于直接赋值,a地址改变,指向{ n: 2 }这个新对象,因此第一个输出为{ n: 2 };
然后执行对a.x赋值,此时只有b仍指向原对象地址,因此相当于对b.x赋值,所以第二个输出为{ n: 1, x: { n: 2 } }。
25. 宏任务和微任务
宏任务:setTimeout,setInterval,setImmediate,requestAnimationFrame,ajax请求,事件监听函数,回调函数,I/O任务
微任务:Promise.then,process.nextTick,MutationObserver
26.数组this指向问题
function f1() {
console.log(this);
}
var arr = [f1, 2, 3];
arr[0](); //(3) [ƒ, 2, 3]
数组调用函数,this指向数组本身
27. 正则表达式
匹配标识符
- /i 执行大小写不敏感匹配
- /g 执行全局匹配
- /m 执行多行匹配
元字符
- \d 匹配数字
- \D 匹配非数字
- \s 匹配空格
- \S 匹配非空格
- \b 匹配单词边缘
- . 匹配任何除换行和结束符以外的字符(通过转义, \.才能匹配.)
- \n 匹配换行符
- \r 匹配回车符
量词
- n* 匹配0个或多个n
- n? 匹配0个或1个n
- n+ 匹配1个或多个n
- n{X} 匹配X个n
- n{X,} 匹配n至少出现X次
- n{X, Y} 匹配n至少出现X次,至多出现Y次
- n$ 匹配任何以n字符结尾的字符串
- ^n 匹配任何以n开头的字符串
- m(?=n) 匹配任何m后紧随n的字符串
- m(?!n) 匹配任何m后不紧随n的字符串
表达式
- [abc] 匹配包含a或b或c
- [^abc] 匹配任何不在方括号内的字符
- [0-9] 匹配包含0-9的数字
- (x|y) 匹配包含x或y
方法
- test() 测试是否包含该正则表达式,返回布尔值,true或false
var reg = /a[bc]d/;
var str1 = "abd";
var str2 = "acd";
var str3 = "ad";
var str4 = "add";
console.log(reg.test(str1)); //true
console.log(reg.test(str2)); //true
console.log(reg.test(str3)); //false
console.log(reg.test(str4)); //false
- exec() 查找匹配结果,若有则返回一个数组,否则返回null
var reg = /a[bc]d/;
var str1 = "abdefacd";
var str2 = "abefbe";
console.log(reg.exec(str1)); //["acd", index: 0, input: "acdefabd", groups: undefined]
console.log(reg.exec(str2)); //null
当正则含有/g标识符时,执行多次会依次返回每个符合正则的结果:
var reg = /a[bc]d/g;
var str1 = "abdefacd";
var str2 = "abefbe";
console.log(reg.exec(str1)); //["abd", index: 0, input: "abdefacd", groups: undefined]
console.log(reg.exec(str1)); //["acd", index: 5, input: "abdefacd", groups: undefined]
支持正则表达式的字符串方法
- search() 返回第一次匹配到的索引值index
var reg = /abc/g;
var str = "dabcefgabch";
console.log(str.search(reg)); //1
- match() 返回所有匹配到的结果,数组形式
var reg = /abc/g;
var str = "dabcefgabch";
console.log(str.match(reg)); //["abc", "abc"]
- replace() 替换所有匹配到的结果
var reg = /abc/g;
var str = "dabcefgabch";
console.log(str.replace(reg), "xyz"); //dxyzefgxyzh
console.log(str); //dabcefgabch, 可见replace方法并不改变原字符串
*split() 将字符串按匹配到的正则表达式切割成数组
var reg = /abc/g;
var str = "dabcefgabch";
console.log(str.split(reg)); //["d", "efg", "h"]
console.log(str); //dabcefgabch, 可见split方法也不改变原字符串
分组
var reg = /(w+)\.(b.*)\.(com)/;
var str = 'www.baidu.com';
console.log(str.replace(reg, '$3.$2.$1')); //com.baidu.www
console.log(RegExp.$1); //www
console.log(RegExp.$2); //baidu
console.log(RegExp.$3); //com
28.递归深拷贝
function deep_clone(obj) {
let buf;
if(obj instanceof Array){
buf = [];
let length = obj.length;
for(i=0; i<length; i++)
buf[i] = deep_clone(obj[i]);
return buf;
} else if(obj === null){
buf = null;
return buf;
} else if(obj.constructor === RegExp){
buf = obj;
return buf;
} else if(obj instanceof Object){
buf = {};
for(let i in obj){
buf[i] = deep_clone(obj[i])
}
return buf;
} else {
buf = obj;
return buf;
}
}
29. 原生实现call, apply, bind
// 实现call
Function.prototype.myCall = function() {
let fn = this;
if(typeof fn !== "function") {
throw TypeError("Caller must be a function");
}
let context = arguments[0];
let args = [...arguments].slice(1);
context.fn = fn; //需要将fn挂载到调用的对象上,否则无法通过对象调用
let res = context.fn(...args); //上一步若不挂载,则无法通过context.fn调用fn函数
delete context.fn; // 将挂载的函数属性删除,否则该对象会新增一个函数属性
return res;
}
// 实现apply
Function.prototype.myApply = function() {
let fn = this;
if(typeof fn !== "function") {
throw TypeError("Caller must be a function");
}
let context = arguments[0];
let args = arguments[1] || []; //apply传进来只有两个参数,第一个是需要绑定的上下文,第二个是函数fn实参数组,由于fn函数可能没有形参,因此此时需将args设为空数组
context.fn = fn;
let res = context.fn(...args);
delete context.fn;
return res;
}
// 实现bind
Function.prototype.myBind = function() {
let fn = this;
if(typeof fn !== 'function') {
throw TypeError("Caller must be a function");
}
let context = arguments[0];
let args = [...arguments].slice(1);
return function() {
let newArgs = [...arguments];
fn.apply(context, [...args, ...newArgs]);
}
}
30. 数组去重,按照用户指定方法
function unique(arr, fn) {
if(typeof fn !== 'function') {
return [...new Set(arr)];
}
let len = arr.length;
let map = new Map();
for(let i=0; i<len; i++) {
let key = fn(arr[i]);
if(map.get(key)) {
arr.splice(i, 1);
i--;
len--;
} else {
map.set(key, true);
}
}
return arr;
}
31.数据劫持
// defineProperty
Object.keys(srcObj).forEach(key => {
let val = srcObj[key];
Object.defineProperty(srcObj, key, {
enumerable: true,
configurable: true,
get() {
return val;
},
set(newVal) {
val = newVal;
return true;
}
});
});
// proxy:
let dstObj = new Proxy(srcObj, {
get(target, key) {
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
return true
}
})
32.事件监听
Event = {
//页面加载完成后
readyEvent: function(fn){
if(fn === null){
fn = document;
}
let oldonload = window.onload;
if(typeof window.onload !== 'function') {
window.onload = fn;
}else{
window.onload = function(){
oldonload();
fn();
}
}
},
//添加监听事件
addEvent: function(element, type, fn) {
if(element.addEventListener){
element.addEventListener(type, fn, false); //第三个参数决定是否捕获
}else if(element.attachEvent){
element.attachEvent('on'+type, fn);
}else {
element['on' + type] = fn;
}
},
//移除监听事件
removeEvent: function(element, type, fn) {
if(element.removeEventListener){
element.removeEventListener(type, fn, false);
}else if(element.detachEvent) {
element.detachEvent('on'+type, fn)
}else {
element['on'+type] = null;
}
},
//阻止事件冒泡
stopPropagation: function(event) {
if(event.stopPropagation){
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
//取消事件默认行为
preventDefault: function(event) {
if(event.preventDefault){
event.preventDefault();
} else {
event.returnValue = false;
}
},
//获取事件目标
getTarget: function(event) {
let target = event.target || event.srcElement;
return target;
},
//获取事件
getEvent: function(event) {
var ev = event || window.event;
if(!ev) {
let c = this.getEvent.caller;
while(c){
ev = c.arguments[0];
if(ev && (Event === ev.constructor || MouseEvent === ev.constructor))
break;
c = c.caller;
}
}
return ev;
}
}
33.防抖和节流
// 防抖是一段时间内事件不被重复触发,然后执行
function fangdou(fn, delay) {
let timer = null;
return function () {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn()
}, delay);
}
}
// 节流是周期执行,每过一段固定时间执行一次
function jieliu(fn, delay) {
let timer = null;
return function() {
if(!timer) {
timer = setTimeout(() => {
fn();
timer = null;
}, delay)
}
}
}
防抖函数的this及传参
function fangdou(fn, delay) {
let timer = null;
return function() {
let args = [...arguments];
console.log(args);
if(timer) { clearTimeout(timer) };
timer = setTimeout(() => {
fn(...args);
console.log(this);
}, delay)
}
}
function fn(a, b) {
console.log(a);
console.log(b);
}
let btn = document.getElementById("btn")
btn.addEventListener("click", fangdou(fn, 1000).bind(btn, 10, 20)) // 10, 20, <button id="btn">按钮</button>
34.CommonJS和ES Module的区别
CommonJS:
- CommonJS是对引入数据赋值。基本数据类型是拷贝,修改单个引用文件中的值不会影响其他引入文件中的值;引用数据类型是引用同一地址,在任何一个引用文件中修改,其它引入文件中的值也会发生变化。
// b.js
let count = 1
let plusCount = () => {
count++
}
setTimeout(() => {
console.log('b.js-1', count)
}, 1000)
setTimeout(() => {
console.log('b.js-2', count)
}, 3000)
module.exports = {
count,
plusCount
}
// a.js
let mod = require('./b.js')
console.log('a.js-1', mod.count)
mod.plusCount()
console.log('a.js-2', mod.count)
setTimeout(() => {
mod.count = 3
console.log('a.js-3', mod.count)
}, 2000)
//输出为:
// a.js-1 1
// a.js-2 1
// b.js-1 2
// a.js-3 3
// b.js-2 2
// 可见基本类型值的修改不会相互影响
// b.js
let obj = {
name: "John"
}
let setObj = () => {
obj.name = "Jenny";
}
setTimeout(() => {
console.log('b.js-1', obj)
}, 1000)
setTimeout(() => {
console.log('b.js-2', obj)
}, 3000)
module.exports = {
obj,
setObj
}
// a.js
let mod = require('./b.js')
console.log('a.js-1', mod.obj)
mod.setObj();
console.log('a.js-2', mod.obj)
setTimeout(() => {
mod.obj.name = "Smith" ;
console.log('a.js-3', mod.obj)
}, 2000)
// 输出为:
// a.js-1 { name: 'John' }
// a.js-2 { name: 'Jenny' }
// b.js-1 { name: 'Jenny' }
// a.js-3 { name: 'Smith' }
// b.js-2 { name: 'Smith' }
// 可见引用类型值的修改会相互影响
- 当CommonJS使用require引入某模块时就会运行该模块的代码,而当重复引入某模块时,该模块代码不会重复执行,而是去缓存取值,除非手动清除缓存
- 一旦某个模块被循环加载,只会输出已执行的部分,未执行的部分不会被输出。
// b.js
exports.done = false
let a = require('./a.js')
console.log('b.js-1', a.done)
exports.done = true
console.log('b.js-2', '执行完毕')
// a.js
exports.done = false
let b = require('./b.js')
console.log('a.js-1', b.done)
exports.done = true
console.log('a.js-2', '执行完毕')
// c.js
let a = require('./a.js')
let b = require('./b.js')
console.log('c.js-1', '执行完毕', a.done, b.done)
// 执行node c.js,输出:
// b.js-1 false
// b.js-2 执行完毕
// a.js-1 true
// a.js-2 执行完毕
// c.js-1 执行完毕 true true
// 解释:
// 遇到requi('./a.js'),去执行a模块代码;
// 执行遇到require('./b.js'),去执行b模块代码;
// 再次遇到require('./a.js'),去执行啊模块代码,执行exports.done = false这一句后返回;
// 输出b.js-1 false与b.js-2 执行完毕,b模块执行完毕,返回a模块;
// 输出a.js-1 true与a.js-2 执行完毕,a模块执行完毕,返回c;
// c遇到require('./b.js'),由于b模块已被require一次,因此不再重复执行,直接从缓存中读取,输出c.js-1 执行完毕 true true。
ES Module:
- 对于import来说,引入模块为动态只读引用。在遇到import时会生成一个引入模块的引用,当执行代码遇到时,根据该引用到引入模块内部去取值。当原始值发生变化时,无论为何种数据类型,引用都会随之发生变化。
- 只读引用的“只读”,类似于const定义变量。
对于基本类型来说,不允许被修改值;
对于引用类型来说,不允许被重新赋值,但允许修改其中属性值;
这是因为对于const来说,不允许修改的只是变量指向的内存中的值。对于基本类型,变量指向的内存中存储的就是该变量值;而对于引用类型来说,该内存存储的只是该引用类型的引用地址,只要该引用地址不发生变化,该引用地址指向的变量的内容是可以发生变化的。 - 当遇到循环引用时,只要双方都有引用,代码就能执行。
35.如何使得定义的引用类型值绝对不可修改
利用Object.freeze
function freezeObj(obj) {
Object.freeze(obj);
for(let key in obj) {
if(typeof obj[key] === 'object') {
freezeObj(obj[key]);
}
}
}
36.Map
Map的键值可以为数字, 字符串, 对象等类型
Map操作方法:
- has(key) 判断是否含有该key
- get(key) 获取该key对应的值
- set(key, val) 给该key赋值
- delete(key) 删除该key
- keys() 获取该Map的所有键值, 返回的为一个Iterator对象, 可以通过next()去访问每个键值
Map是有顺序的, 它会按照set的顺序存储键值对