跳到主要内容

输出题

类型输出

  1. typeof 之后是''的字符串
typeof typeof 1
console.log(typeof typeof 1) ‘string’
typeof NaN   //‘number'

2.包装对象

console.log(Number(2)===2); //true
console.log(Symbol('foo')===Symbol('foo')); //false
console.log(Boolean(false)===Boolean(false)); //ture
console.log(NaN === NaN); //false
console.log([] === []); //false
console.log(typeof new String());  //object
console.log(typeof String()); //string
console.log(typeof String); //function

异步和事件循环

基本顺序

const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
1 
2
4

promise.then 是微任务,它会在所有的宏任务执行完之后才会执行,同时需要promise内部的状态发生变化,因为这里内部没有发生变化,一直处于pending状态,所以不输出3。

promise状态

const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);
promise1
1 Promise{<resolved>: resolve1}
2 Promise{<pending>}
resolve1

直接打印promise1,会打印出它的状态值和参数

代码执行过程如下:

  1. script是一个宏任务,按照顺序执行这些代码;
  2. 首先进入Promise,执行该构造函数中的代码,打印promise1
  3. 碰到resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来;
  4. 碰到promise1.then这个微任务,将它放入微任务队列;
  5. promise2是一个新的状态为pendingPromise
  6. 执行同步代码1, 同时打印出promise1的状态是resolved
  7. 执行同步代码2,同时打印出promise2的状态是pending
  8. 宏任务执行完毕,查找微任务队列,发现promise1.then这个微任务且状态为resolved,执行它。

promsie和setTimeout

setTimeout(() => {
console.log(7)
}, 0)
new Promise((resolve, reject) => {
console.log(3);
resolve();
console.log(4);
}).then(() => {
console.log(6)
})
console.log(5)

3
4
5
6
7

promise.then()返回新的promsie

setTimeout(() => {
console.log(7)
}, 0)
new Promise((resolve, reject) => {
console.log(3);
resolve();
console.log(4);
}).then(() => {
console.log(6)
}).then(() => {
console.log(8);
}).then(() => {
console.log(9);
})
console.log(5)

3
4
5
6
8
9
7

promise套promise

new Promise(resolve => {
console.log('Promise1');
resolve()
}).then(()=>{
console.log('then11')
new Promise(resolve => {
console.log('Promise2');
resolve()
}).then(()=>{
console.log('then21');
}).then(()=>{
console.log('then23');
})
}).then(()=>{
console.log('then12');
})
Promise1
then11
Promise2
then21
then12
then23

then21和then12属于一轮

promise套setTimeout

const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
1
2
4
timerStart
timerEnd
success

代码执行过程如下:

  • 首先遇到Promise构造函数,会先执行里面的内容,打印1
  • 遇到定时器steTimeout,它是一个宏任务,放入宏任务队列;
  • 继续向下执行,打印出2;
  • 由于Promise的状态此时还是pending,所以promise.then先不执行;
  • 继续执行下面的同步任务,打印出4;
  • 此时微任务队列没有任务,继续执行下一轮宏任务,执行steTimeout
  • 首先执行timerStart,然后遇到了resolve,将promise的状态改为resolved且保存结果并将之前的promise.then推入微任务队列,再执行timerEnd
  • 执行完这个宏任务,就去执行微任务promise.then,打印出resolve的结果。

setTimeout套promise

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

代码执行过程如下:

  1. 首先,Promise.resolve().then是一个微任务,加入微任务队列
  2. 执行timer1,它是一个宏任务,加入宏任务队列
  3. 继续执行下面的同步代码,打印出start
  4. 这样第一轮宏任务就执行完了,开始执行微任务Promise.resolve().then,打印出promise1
  5. 遇到timer2,它是一个宏任务,将其加入宏任务队列,此时宏任务队列有两个任务,分别是timer1timer2
  6. 这样第一轮微任务就执行完了,开始执行第二轮宏任务,首先执行定时器timer1,打印timer1
  7. 遇到Promise.resolve().then,它是一个微任务,加入微任务队列
  8. 开始执行微任务队列中的任务,打印promise2
  9. 最后执行宏任务timer2定时器,打印出timer2
(() => {
setTimeout(() => {
console.log("1-1");
Promise.resolve().then(() => {
console.log("1-2");
});
});
console.log("2-1");
Promise.resolve().then(() => {
console.log("3-1");
setTimeout(() => {
console.log("3-2");
});
});
new Promise(function (reslove) {
console.log('4-1');
reslove();
}).then(function () {
console.log('4-2');
})
})()

2-1
4-1
3-1
4-2
1-1
1-2
3-2

状态改变

const promise = new Promise((resolve, reject) => {
resolve('success1');
reject('error');
resolve('success2');
});
promise.then((res) => {
console.log('then:', res);
}).catch((err) => {
console.log('catch:', err);
})
then:success1

Promise的状态在发生变化之后,就不会再发生变化。开始状态由pending变为resolve,说明已经变为已完成状态,下面的两个状态的就不会再执行,同时下面的catch也不会捕获到错误。

const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
promise1 Promise {<pending>}
promise2 Promise {<pending>}

Uncaught (in promise) Error: error!!!
promise1 Promise {<fulfilled>: "success"}
promise2 Promise {<rejected>: Error: error!!}

只有执行了resolve()或者reject()状态才会发生改变

Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).catch(err => {
console.log('catch', err)
})
error err!!!

.then函数中的两个参数:

  • 第一个参数是用来处理Promise成功的函数
  • 第二个则是处理失败的函数

也就是说Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。

在这道题中,错误直接被then的第二个参数捕获了,所以就不会被catch捕获了,输出结果为:error err!!!'

链式调用传值

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(1)
.then(res => {
console.log(res);
throw err
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
1
3

上面的输出结果之所以依次打印出1和3,是因为resolve(1)之后走的是第一个then方法,抛出错误进catch里,所以第二个then中的res得到的实际上是第catch的返回值。并且return 3会被包装成reject(3),被最后的then打印输出3

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的结果会传递下面

好多的then,实际上只需要记住一个原则:.then.catch 的参数期望是函数,传入非函数则会发生值透传

第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1) 的值直接传到最后一个then里,直接打印出1

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 本身,否则会造成死循环。

finally

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()方法不管Promise对象最后的状态如何都会执行
  • .finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是无法知道Promise最终的状态是resolved还是rejected
  • 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。
  • finally本质上是then方法的特例
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中抛出的异常
var p = new Promise(resolve => {
console.log(4);
resolve(5);
});
function func1() {
console.log(1);
function func2() {
setTimeout(() => {
console.log(2);
});
}
func2();
}
func1();
console.log(3);
p.then(resolved => {
console.log(resolved)
}
).then(() => {
console.log(6)
});

this指向

简单函数调用

function show(){
console.log(this);
}
show() //Window{}
var obj={
show:function show(){
console.log(this);
}
}
obj.show()//obj{}

对象内函数调用函数

function foo() {
console.log( this.a );
}

function doFoo() {
foo();
}

var obj = {
a: 2,
doFoo: doFoo
};

var a = 1;
obj.doFoo()

//1

在Javascript中,this指向函数执行时的当前对象。在执行foo的时候,执行环境就是doFoo函数,执行环境为全局。所以,foo中的this是指向window的,所以会打印出1。

var name='1';
var obj={
name:'2',
fun1L:function(){
function func2(){
console.log(this.name);
}
func2()
}
}
obj.fun1L()//1

对象内函数返回函数

 var obj={
show:function show(){
return function show2(){
console.log(this);
}

}
}
obj.show()() //window{}
var show3=obj.show()
show3() //window{}
 var obj={
show:function show(){
return()=>{
console.log(this);
}

}
}
obj.show()() //obj{}
var show3=obj.show()
show3() //obj{}

对象内箭头函数调用

var obj = {
say: function() {
var f1 = () => {
console.log("1111", this);
}
f1();
},
pro: {
getPro:() => {
console.log(this);
}
}
}
var fn = obj.say;
fn();
obj.say();
obj.pro.getPro();

//1111 window{}
//1111 obj{}
//window{}
  1. fn(),fn是在全局执行的,而f1是箭头函数,它是没有绑定this的,它的this指向其父级的this,其父级say方法的this指向的是全局作用域,所以会打印出window;
  2. obj.say(),谁调用say,say 的this就指向谁,所以此时this指向的是obj对象;
  3. obj.pro.getPro(),我们知道,箭头函数时不绑定this的,getPro处于pro中,而对象不构成单独的作用域,所以箭头的函数的this就指向了全局作用域window。

对象内普通函数和箭头函数

var a = 10
var obj = {
a: 20,
say: () => {
console.log(this.a)
}
}
obj.say()

var anotherObj = { a: 30 }
obj.say.apply(anotherObj)
// 10 10

我么知道,箭头函数时不绑定this的,它的this来自原其父级所处的上下文,所以首先会打印全局中的 a 的值10。后面虽然让say方法指向了另外一个对象,但是仍不能改变箭头函数的特性,它的this仍然是指向全局的,所以依旧会输出10。

如果是普通函数,那么就会有完全不一样的结果:

var a = 10  
var obj = {
a: 20,
say(){
console.log(this.a)
}
}
obj.say()
var anotherObj={a:30}
obj.say.apply(anotherObj)
//20 30

say方法中的this就会指向他所在的对象,输出其中的a的值

call绑定null

function a() {
console.log(this);
}
a.call(null);
//Window{}

如果第一个参数传入的对象调用者是null或者undefined,call方法将把全局对象(浏览器上是window对象)作为this的值。所以,不管传入null 还是 undefined,其this都是全局对象window。所以,在浏览器上答案是输出 window 对象。

要注意的是,在严格模式中,null 就是 null,undefined 就是 undefined

'use strict';

function a() {
console.log(this);
}
a.call(null); // null
a.call(undefined); // undefined

new构造函数

var obj = { 
name : 'cuggz',
fun : function(){
console.log(this.name);
}
}
obj.fun() // cuggz
new obj.fun() // undefined

使用new构造函数时,其this指向的是全局环境window。

function foo(something){
this.a = something
}

var obj1 = {
foo: foo
}

var obj2 = {}

obj1.foo(2);
console.log(obj1.a); // 2

obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3

var bar = new obj1.foo(4)
console.log(obj1.a); // 2
console.log(bar.a); // 4

  1. 首先执行obj1.foo(2); 会在obj中添加a属性,其值为2。之后执行obj1.a,a是右obj1调用的,所以this指向obj,打印出2;
  2. 执行 obj1.foo.call(obj2, 3) 时,会将foo的this指向obj2,后面就和上面一样了,所以会打印出3;
  3. obj1.a会打印出2;
  4. 最后就是考察this绑定的优先级了,new 绑定是比隐式绑定优先级高,所以会输出4。
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

主要都是考察this绑定的优先级。记住以下结论即可:**this绑定的优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

对象内的立即执行函数

var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log(this.foo); //bar
console.log(self.foo); //bar
(function() {
console.log(this.foo); //undefined
console.log(self.foo); //bar
}());
}
};
myObject.func();
//bar bar undefined bar
  1. 首先func是由myObject调用的,this指向myObject。又因为var self = this;所以self指向myObject。
  2. 这个立即执行匿名函数表达式是由window调用的,this指向window 。立即执行匿名函数的作用域处于myObject.func的作用域中,在这个作用域找不到self变量,沿着作用域链向上查找self变量,找到了指向 myObject对象的self。
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
  1. 执行db1()时,this指向全局作用域,所以window.number 4 = 8,然后执行匿名函数, 所以window.number 5 = 40;
  2. 执行obj.db1();时,this指向obj对象,执行匿名函数,所以obj.numer * 5 = 15。
var x = 3;
var y = 4;
var obj = {
x: 1,
y: 6,
getX: function() {
var x = 5;
return function() {
return this.x;
}();
},
getY: function() {
var y = 7;
return this.y;
}
}
console.log(obj.getX()) // 3
console.log(obj.getY()) // 6
  1. 我们知道,匿名函数的this是指向全局对象的,所以this指向window,会打印出3;
  2. getY是由obj调用的,所以其this指向的是obj对象,会打印出6。

函数当参数传递

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
  1. 第一次执行fn(),this指向window对象,输出10。
  2. 第二次执行arguments[0],相当于arguments调用方法,this指向arguments,而这里传了两个参数,故输出arguments长度为2。
var a = 1;
function printA(){
console.log(this.a);
}
var obj={
a:2,
foo:printA,
bar:function(){
printA();
}
}

obj.foo(); // 2
obj.bar(); // 1
var foo = obj.foo;
foo(); // 1
  1. obj.foo(),foo 的this指向obj对象,所以a会输出2;
  2. obj.bar(),printA在bar方法中执行,所以此时printA的this指向的是window,所以会输出1;
  3. foo(),foo是在全局对象中执行的,所以其this指向的是window,所以会输出1;

重复声明

 var a = 10; 
var obt = {
a: 20,
fn: function(){
var a = 30;
console.log(this.a)
}
}
obt.fn(); // 20
obt.fn.call(); // 10
(obt.fn)(); // 20
  1. obt.fn(),fn是由obt调用的,所以其this指向obt对象,会打印出20;
  2. obt.fn.call(),这里call的参数啥都没写,就表示null,我们知道如果call的参数为undefined或null,那么this就会指向全局对象this,所以会打印出 10;
  3. (obt.fn)(), 这里给表达式加了括号,而括号的作用是改变表达式的运算顺序,而在这里加与不加括号并无影响;相当于 obt.fn(),所以会打印出 20;

this返回

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

  1. 最关键的就是var x = a(5),函数a是在全局作用域调用,所以函数内部的this指向window对象。所以 this.x = 5 就相当于:window.x = 5。之后 return this,也就是说 var x = a(5) 中的x变量的值是window,这里的x将函数内部的x的值覆盖了。然后执行console.log(x.x), 也就是console.log(window.x),而window对象中没有x属性,所以会输出undefined。
  2. 当指向y.x时,会给全局变量中的x赋值为6,所以会打印出6。

作用域和变量提升

不使用var let const

说明: 1.使用var声明变量,在方法内部是局部变量,在方法外部是全局变量 2.没有使用var声明的变量,在方法内部或外部都是全局变量,但如果是在方 法内部声明,在方法外部使用之前需要先调用方法,告知系统声明了全局变量后方可在方法外部使用。

在函数作用域内 加var定义的变量是局部变量,不加var定义的就成了全局变量 在function内部,加var的是局部变量,不加var的则是全局变量; 在function外部,不管有没有使用var声明变量,都是全局变量,在function外部,var关键字一般可以省略,但是为了书写规范和维护方便以及可读性好,不建议省略var关键字

var常见问题

允许重复声明,导致变量被覆盖
var a=1;
var a=2;
var a=3;
console.log(a); //3
var a = 1;
function prinf(){
console.log(a);
}
var a = 2;
prinf();//2

这里假设你var a = 2前面还有上万行代码,那么你调用prinf函数的时候,是想打印之前的a,但是你忘记之前声明的变量和几万行代码后面的a是同名的,导致后面声明的a覆盖了之前的a.

console.log(a)  //undefined
var a=1;
console.log(a) //1
var a=2
console.log(a) //2

特殊情况

var a = 1;
function prinf(){
console.log(a);
var a = 2;
}

prinf();//undefined
let a = 1;
function prinf(){
console.log(a);
let a = 2;
}

prinf();//Identifier 'a' has already been declared
全局变量挂载到全局对象:全局对象成员污染问题
var a=1;
console.log(window.a); //1
var console = "打印";
console.log(console);
//console.log is not a function

let使用

不能重复声明

let a=1;
var a=2;
console.log(a) //SyntaxError: Identifier 'a' has already been declared

初始化问题(先声明,再赋值) 报错

a=1;
let a=2;
console.log(a)//Uncaught ReferenceError: Cannot access 'a' before initialization
let a=1;
a=2;
console.log(a) //2

基本原理

1.外层的变量函数内部可以找到,函数内部的变量(局部变量)外层找不到。

由于a是函数内部定义的局部变量,根据作用域关系,外层(全局环境)访问不到局部变量。因此会报错

function aaa() {
var a = 10;
}
alert(a);//Error a is not defined
var a=10;
function aaa(){
alert(a);
}

function bbb(){
var a=20;
aaa();
}
bbb();//10 由于aaa()执行时aaa的作用域链中a=10;

2.当var不加时,会自动生成全局变量(不建议这样写,一般定义变量时建议使用var关键字)

function aaa() {
var a = b = 10;//b未用var定义相当于全局变量,所以在函数外能够被访问到
}
aaa();
//alert(a);//会报错,a is not defined
alert(b);//10 相当于全局变量可以访问

//此时上面代码相当于以下代码
var b;
function aaa() {
b = 10;
var a = b;
}
aaa();
alert(a);//Error
alert(b);//10

3.变量的查找是就近原则,找就近var定义的,就近找不到的话则在外层寻找。

在内部找到了a 虽然是undefine

var a=10;
function aaa(){
alert(a);
var a=20;
}
aaa();//undefined 就近原则找到var定义的,预解析过程

4.当参数和局部变量重名时,优先级等同

var a=10;
function aaa(a){
alert(a);
var a=20;
}
aaa(a);//10 由于参数和局部变量优先级等同,所以查找时找到局部变量10然后再是var定义的局部变量

局部变量将参数覆盖

var a=10;
function aaa(a){
var a=20;
alert(a);

}
aaa(a);//20 局部变量将参数覆盖了,因此a值为局部变量的值

基本类型

var a=10;
function aaa(a){
a+=3;//基本类型 /参数相当于局部变量,只是局部修改了a的值,而在全局访问时依然是全局的值
}
aaa(a);
alert(a);//10

引用类型是引用的地址

var a=[1,2,3];var b=[1,2,3];
function aaa(a,b){
a.push(4);     b=[1,2,3,4];
}
aaa(a,b);
alert(a);//[1,2,3,4] 引用类型是引用的地址,所以局部修改会影响全局的值 alert(b);//[1,2,3]

如果一个变量名没有被声明过,且没有使用 var 就赋予了初始值,它就会成为全局作用域下的变量

function setA() {
a = 1;
}
setA();
console.log(a); // 1

如果多次声明同一个变量, 之后的 var 声明会被忽略掉

var a = 1;
var a = 2;
var a = 3;
console.log(a)
//3

等价于

var a = 1;
a = 2;
a = 3;
console.log(a)
//3

函数声明你也可以认为是一个 var 声明,千万不要以为后面的 var 声明能够覆盖掉同名的函数声明

变量提升问题

变量声明和函数声明都是会发生 提升 (hoist)的。

怎么理解,就是在执行代码前,会先扫描一下代码,将其中类似 var a = 1 的语句中声明的部分,也就是 var a 先找出来,先执行。

这是编译器的行为,所以在表现上,就是声明被放到了代码的开头,成为 提升

除了 var / let / const 声明的变量会提升,函数声明也会提升,且函数声明会优先于变量提升,即放到变量声明的上方

b();
var a = 1;
function b() { console.log(a) };

等价于

function b() { console.log(a) };
var a;

b(); // 输出 undefined
a = 1;
var a=10;
function fn(){
console.log(a) //暂时性死区 变量提升
var a=20
}
fn()//undefined

有一个易错点,就是把函数表达式当成了函数声明。函数表达式是不会发生变量提升的,切记

function fn(a) {
console.log(a);
var a = 2;
function a() { }
console.log(a);
}
fn(1);

首先是函数的参数 a,它等价于一个外部的一个变量,上面代码等价于:

var a = 1;

(function() {
function a() { }

console.log(a);
a = 2;
console.log(a);
})();

这里函数声明做了提升,跑到最顶部。接下来考虑给 var a = 2 做提升。但我们的函数 a 的声明已经提升了,所以这里的 var 算是重复声明了,直接去掉 var,最终得到上述代码。

我们很容易看出答案是:

ƒ a() { }
2

本题除了考察比较常规的声明提升,还考察了一个比较特别的情况,就是 函数的参数声明应该放在哪里

答案是放到一个额外的包裹着函数体的中间层中,而不是直接放到函数体的顶部。

var a=8;
function foo(){
var a=6
return function(){
a=5
}
}
foo()()
console.log(a)//8

作用域题目

立即执行函数作用域

(function (args) {
statement;
})(args)
  1. 该函数表达式在定义时就会调用
  2. 该函数将拥有自己独立的词法作用域,外部无法访问
  3. 该函数可以访问到外部的变量
var a = 0; // 可以被立即执行函数访问
(function () {
console.log(a); // 0
var b = 1; // 不能被外部访问
})()
console.log(b); // ReferenceError: b is not defined
var a = 0; // 可以被立即执行函数访问
(function () {
console.log(a); // 0
b = 1; // 能被外部访问
})()
console.log(b); // 1

var a = 10;
(function(){
var a;
console.log(window.a)
console.log(a)
a=20
console.log(a)
})()

//10
//undefined
//20
//undefined

对象嵌套

var b = {
a,
c: b
}
console.log(b)

{
"a": 10,
"c": {
"a": 10,
"c": {
"a": 10
}
}
}

image-20220712184525989

1.var循环中的setTimeout问题

for(var i = 0; i < 5; i++){

setTimeout(()=>{

console.log(i);

},1000)

}

5,5,5,5,5

因为setTimeout是异步执行,每一次for循环的时候,setTimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里,等待执行。只有主线上的任务执行完,才会执行任务队列里的任务。也就是说它会等到for循环全部运行完毕后,才会执行fun函数,但是当for循环结束后此时i的值已经变成了5,因此虽然定时器跑了5秒,控制台上的内容依然是5

用闭包方式改写

for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, 1000)
})(i);
}
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
i * 1000,
i
)
}
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i); // i 指向全局的 i,也就是数组中函数所有的i都指向的是同一个变量i
};
}
a[6](); // 10

在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会自增,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是循环结束之后i的值,也就是 10。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6

这是因为let 创建的变量是块级作用域的,所以每次循环都是一个新的 i,每次的值都是 i++ 的结果。因为每次循环的 i 都是一个独立的变量(内存里的唯一地址),因此闭包记录的值都是唯一的,所以才能得到最终的结果

如果用 var 的话,变量 i 是一个全局变量,虽然循环体内每次都创建了一个函数来打印 i,但是当时当刻仅仅是一个指向全局变量 i 的指针,当循环结束之后无论你用哪一个下标去访问循环创建的闭包函数,打印的变量 i 都是全局的那一个,所以全部都是 10。

2.立即执行函数作用域

(function(){
var x = y = 1;
})();
var z;

console.log(y); // 1
console.log(z); // undefined
console.log(x); // Uncaught ReferenceError: x is not defined

这段代码的关键在于:var x = y = 1; 实际上这里是从右往左执行的,首先执行y = 1, 因为y没有使用var声明,所以它是一个全局变量,然后第二步是将y赋值给x,讲一个全局变量赋值给了一个局部变量,最终,x是一个局部变量,y是一个全局变量,所以打印x是报错。

var a = 100;
function create(){
var a = 200;
return function(){
console.log(a + 'create');
}
}
var fn2 = create();
fn2();
//200 create

3.函数和变量同名

console.log(x);
x()
var x=2;
function x(){
console.log(1);
}
console.log(x);
x=9;
x()

输出结果

ƒ x(){
console.log(1);
}
1
2
Uncaught TypeError: x is not a function

5.作用域和this

let obj={
num:117
}
var num=935;
function func(){
console.log(this.num);
}
const wrapper=func.bind(obj)
setTimeout(() => {
wrapper()
}, 1000);
obj={
num:130
}
//117

原型继承

1.原型相等

function func1(){}
function func2(){}
func1.prototype=func2.prototype={}
const son=new func1()
console.log(son instanceof func1,son instanceof func2);//true true

2.箭头函数没有prototype

function giveLydiaPizza() {
return "Here is pizza!"
}

const giveLydiaChocolate = () => "Here's chocolate... now go hit the gym already."

console.log(giveLydiaPizza.prototype) //{ constructor: ...}
console.log(giveLydiaChocolate.prototype) //undefined

例如giveLydiaPizza函数,有一个prototype属性,它是一个带有constructor属性的对象(原型对象)。 然而,箭头函数,例如giveLydiaChocolate函数,没有这个prototype属性。 尝试使用giveLydiaChocolate.prototype访问prototype属性时会返回undefined

class Person {
constructor() {
this.name = "Lydia"
}
}

Person = class AnotherPerson {
constructor() {
this.name = "Sarah"
}
}

const member = new Person()
console.log(member.name)
//"Sarah"

我们可以将类设置为等于其他类/函数构造函数。 在这种情况下,我们将Person设置为AnotherPerson。 这个构造函数的名字是Sarah,所以新的Person实例member上的 name 属性是Sarah

其他

1.对象的symbol属性

const info = {
[Symbol('a')]: 'b'
}

console.log(info)
console.log(Object.keys(info))
// {Symbol('a'): 'b'} []

Symbol类型是不可枚举的。Object.keys方法返回对象上的所有可枚举的键属性。Symbol类型是不可见的,并返回一个空数组。 记录整个对象时,所有属性都是可见的,甚至是不可枚举的属性。

这是Symbol的众多特性之一:除了表示完全唯一的值(防止对象意外名称冲突,例如当使用 2 个想要向同一对象添加属性的库时),您还可以隐藏这种方式对象的属性(尽管不完全。你仍然可以使用Object.getOwnPropertySymbols()方法访问 Symbol

2.剩余参数

function getItems(fruitList, ...args, favoriteFruit) {
return [...fruitList, ...args, favoriteFruit]
}

getItems(["banana", "apple"], "pear", "orange") //SyntaxError

... args是剩余参数,剩余参数的值是一个包含所有剩余参数的数组,并且只能作为最后一个参数。上述示例中,剩余参数是第二个参数,这是不可能的,并会抛出语法错误。

function getItems(fruitList, favoriteFruit, ...args) {
return [...fruitList, ...args, favoriteFruit]
}

getItems(["banana", "apple"], "pear", "orange")

上述例子是有效的,将会返回数组:[ 'banana', 'apple', 'orange', 'pear' ]

3.push方法返回的是数组长度

let newList = [1, 2, 3].push(4)

console.log(newList.push(5)) //TypeError

.push方法返回数组的长度,而不是数组本身! 通过将newList设置为[1,2,3].push(4),实际上newList等于数组的新长度:4

然后,尝试在newList上使用.push方法。 由于newList是数值4,抛出 TypeError。

4.默认传参

function test(m = n, n = 2) {
console.log(m, n)
}
test() // Uncaught ReferenceError: Cannot access 'n' before initialization
test(3) // 3 2
test(3, 4) // 3 4

5.[].map(parseInt)

var arr = ['1', '2', '3'];
var r;

r = arr.map(parseInt);

console.log(r); //1, NaN, NaN

概念:以第二个参数为基数来解析第一个参数字符串,通常用来做十进制的向上取整(省略小数)如:parseInt(2.7) //结果为2

特点:接收两个参数parseInt(string,radix)

string:字母(大小写均可)、数组、特殊字符(不可放在开头,特殊字符及特殊字符后面的内容不做解析)的任意字符串,如 '2'、'2w'、'2!'

radix:解析字符串的基数,基数规则如下:

1) 区间范围介于2~36之间,否则返回NaN

2 ) 当参数为 0,parseInt() 会根据十进制来解析;

3 ) 如果忽略该参数,默认的基数规则:

​ 如果 string 以 "0x" 开头,parseInt() 会把 string 的其余部分解析为十六进制的整数;parseInt("0xf") //15

​ 如果 string 以 0 开头,其后的字符解析为八进制或十六进制的数字;parseInt("08") //8

​ 如果 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数;parseInt("88.99f") //88

​ 只有字符串中的第一个数字会被返回。parseInt("10.33") //返回10;

​ 开头和结尾的空格是允许的。parseInt(" 69 10 ") //返回69

​ 如果字符串的第一个字符不能被转换为数字,返回 NaN。parseInt("f") //返回NaN 而parseInt("f",16) //返回15

map 函数将数组的值 value 传递给了 parseInt 的第一个参数,将数组的索引传递给了第二个参数。 第三个参数呢?我们再加一个参数。

a = ["1","2", "3"]
var parseInt = function(string, radix, obj) {
return string + "-" + radix + "-" + obj;
};

console.log(a.map(parseInt)); //[ '1-0-1,2,3', '2-1-1,2,3', '3-2-1,2,3' ]

可以看出,map 确实为 parseInt 传递了三个参数 数组的值,值对应的索引,原数组。

可以看出,map的回调函数的参数index索引值作了parseInt的基数radix,索引index的起始值从0开始。

得出,正是由于map的回调函数的参数index索引值作了parseInt的基数radix,导致出现超范围的radix赋值和不合法的进制解析,才会返回NaN。

["1","2", "3"].map(parseInt)

[0]=parseInt("1",0);//十进制1

[1]=parseInt("2",1);//radix不在2~36的返回NaN

[2]=parseInt("3",2);//二进制数没有3,只有0 1 ,所以NaN