类型转换
题目1
[1,2,3]+[4,5,6]输出什么?
【解析】
+ 运算符作为二元运算符,会将等号两边的值转换成原始类型。具体过程如下:
1、ToPrimitive([1,2,3])。ToPrimitive方法会
- 先调用对象的
valueOf方法,返回的[1, 2, 3]不是原始类型。 - 继续调用对象的
toString方法,toString方法会把数组每个元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串返回。本题目返回'1,2,3'是原始类型。
2、ToPrimitive([4,5,6]),同上。
3、判断等号2边是否有字符串,有直接返回字符串的结果。题目中在这里就返回了。
4、没有的话把等号2边都转成Number
输出结果
1,2,34,5,6
掌握了吗? 再来一道相似的题巩固下是否掌握了)坏笑
[] + {}输出什么?
栈内存和堆内存
题目1
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
a.x // 这时 a.x 的值是多少
b.x // 这时 b.x 的值是多少
【解析】
变量a被赋值了一个引用类型,变量a存在栈中,该值保存了{n: 1}对象的访问地址,该地址与堆内存{n: 1}的实际值关联。
把a赋值给b,在栈内存中会发生数据复制行为,系统会为新变量b分配一个引用类型{n: 1}的地址指针保存在栈内存中。a 和b 访问的是同一份地址指针,也就是堆内存中的同一个对象。
a.x = a = {n: 2}这句先执行的是a.x,因为.的优先级比=高。所以堆内存的{n: 1}对象会变成{n: 1,x:undefined}。
然后执行赋值语句。
赋值语句从右到左,此时相当于顺序执行下面语句
a= {n:2}
{n: 1,x:undefined} = {n: 2}
所以,输出的结果是:
a.x // -> undefined
b.x // -> {n:2}
题目2
代码1
var obj = {
value: 1
};
function foo(o) {
o.value = 2;
console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
代码2
var obj = {
value: 1
};
function foo(o) {
o = 2;
console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1
【解析】
ECMAScript中所有函数的参数都是按值传递的。
也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。
对于基本数据类型,这种复制是值拷贝。对于引用数据类型,这种复制是指针地址的拷贝。
所以,代码1修改了o.value,是通过指针地址,找到原值,直接修改o
代码2是修改了复制之后的参数o1的值,对o没影响。
二、作用域
作用域指的是变量作用的区域,也就是函数和变量的可访问性。
题目1
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。
题目2
你能说出下面2段代码的区别吗?
代码1:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
代码2:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
【解析】
1、以上代码的执行结果一样,但是执行上下文的调用是不一样的。
JS执行一段代码前,会先创建对应的执行上下文,每个执行上下文都有三个重要的属性:
- 变量对象(VO)
- 作用域(Scope chain)
- this
再把上下文压入调用栈, 再进入执行上下文,执行代码。
执行上下文分为三种:
- 全局执行上下文:首次执行JS代码时,会创建一个全局上下文。this执行全局对象。
- 函数执行上下文:在函数被调用时创建。每次调用函数都会创建一个新的执行上下文。
- eval执行上下文。
上面两段代码创建的执行上下文分别如下: 代码1:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
代码2:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
JS函数执行时用到的作用域链,是在函数定义时就创建的。 所以scope一定是局部变量,不管何时执行函数f(),这种绑定在执行f()的时候都有效。
题目3
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
【解析】
在全局作用域中“定义”一个函数到时候,会创建包含全局作用域的作用域链。在只有“执行”该函数的时候,会复制创建时的作用域,并将当前函数的局部作用域放在作用域链的顶端。
所以执行foo的时候,先从函数内部查找是否有局部变量value。
如果没有,就根据函数的定义位置,查找上一层代码。也就是全局的value=1
所以输出结果是1。
题目4
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
【解析】
上面题目2的解析说到
js创建执行上下文之后,会进入执行上下文,开始执行代码。在函数的上下文中,进入函数执行上下文后,用活动对象
(activation object, AO)来表示变量对象(VO)。
变量对象包含:
1、 函数的所有形参
2、 函数声明
3、 变量声明
例如下面这段代码
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
console.log(y)
}
foo(1);
在进入执行上下文后,这时候的 AO 是
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
代码执行阶段,进入执行上下文,顺序执行代码,根据代码,修改变量对象的值。
由于没有y的值,js引擎会去全局的变量环境查找,全局没找到,会报错,
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
三、变量提升
V8执行一段代码之前,需要先对代码进行编译。编译阶段先进行语法分析和词法分析,最后生成编译器可以直接执行的AST。
在语法分析阶段,会为变量和函数开辟内存空间。具体过程如下:
查找函数声明,值赋予函数体(函数声明优先);
查找变量声明,值赋予undefined
变量声明的提升是以变量所处的第一层词法作用域为“单位”的,即
- 全局作用域中声明的变量会提升至全局最顶层
- 函数内声明的变量只会提升至该函数作用域最顶层。
题目1
(function(){
var x = y = 1;
})();
var z;
【解析】
这里的关键在于代码是从右到左执行的,y=1 没有使用var声明。所以y是个全局变量。
结果
console.log(y); // 1
console.log(z); // undefined
console.log(x); // Uncaught ReferenceError: x is not defined
题目2
function a() {
var temp = 10;
function b() {
console.log(temp);
}
b();
}
a();
function a() {
var temp = 10;
b();
}
function b() {
console.log(temp);
}
a();
【解析】
1、这里发生了的a发生了函数提升,第二个a函数声明会覆盖掉第一个,所以,实际上两个a()执行的都是第二个函数声明的函数体。
2、需要明确的是,函数的作用域和作用域链,是在定义的时候就决定的。 所以,第一个a的b函数的声明的作用域如下,作用域里面保存了父级变量的对象。
b.[[scope]] = [
a.AO,
global.Context.Vo
]
但是第二个a函数中的b函数声明里面,创建的作用域如下,作用域里面保存了全局变量的变量对象。所以执行的时候,b创建的执行上下文的活动对象不包含temp,全局变量对象也没有,所以报错了。
b.[[scope]] = [
global.Context.Vo
]
输出
// 报错 Uncaught ReferenceError: temp is not defined
四、this指向
题目1
function foo () {
console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
obj.foo();
foo2();
【解析】
1、obj.foo()这个里发生了隐式类型转换,此时函数this总是指向最后调用它的的对象。所以foo函数this指向调用者obj
2、foo2调用它的是window。相当于window.foo2()。这里发生了隐式丢失。
输出
1
2
题目2
把一个函数当成参数传递时,会发生隐式赋值丢失,且与包裹着它的函数的this无关。在非严格模式下,this指向window,在严格模式下,this指向undefined
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 }
doFoo(obj.foo)
obj2.doFoo(obj.foo)
输出:
{ a:3, doFoo: f }
2
{ a:3, doFoo: f }
2
题目3
var obj1 = {
a: 1
}
var obj2 = {
a: 2,
foo1: function () {
console.log(this.a)
},
foo2: function () {
function inner () {
console.log(this)
console.log(this.a)
}
inner()
}
}
var a = 3
obj2.foo1()
obj2.foo2()
【解析】
函数的调用方式有4种
- 作为一个函数调用:不属于任何一个对象,就是一个函数,这样的情况在 JavaScript 的在浏览器中的非严格模式默认是属于全局对象 window 的,在严格模式,就是 undefined。
- 函数作为对象的方法调用。this指向最后调用它的对象。
- 使用构造函数调用函数:指向实例对象。
- 作为函数方法调用函数(call、apply):指向第一个参数,如果是null或undefined,将使用全局对象替代。
1、obj2.foo1()是第二种调用方式,所以,这里指向的是obj2。输出2
2、obj2.foo2()也是第二种调用方式。foo2()里面执行inner函数,inner()的调用方式是第一种。所以,inner里面的this指向window
输出
2
Window{...}
3
题目4
var name = 'window'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
return function () {
console.log(this.name)
}
}
this.foo2 = function () {
console.log(this.name)
return () => {
console.log(this.name)
}
}
this.foo3 = () => {
console.log(this.name)
return function () {
console.log(this.name)
}
}
this.foo4 = () => {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
person1.foo1()() // 'person1' 'window'
person1.foo2()() // 'person1' 'person1'
person1.foo3()() // 'person1' 'window'
person1.foo4()() // 'person1' 'person1'
题目5
this绑定的优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
function foo(something){
this.a = something
}
var obj1 = {
foo: foo
}
var bar = new obj1.foo(4)
console.log(bar.a); // 4
【解析】
这里考察的是this绑定的优先级,同时发生了new绑定和隐式绑定。new绑定的优先级高于隐式绑定。
bar的结果实际上是{a:4}。所以结果输出4。
题目6
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo();
})();
【解析】
调用匿名函数的是的是window。 这里只在立即执行函数里面使用了严格模式,改变了立即执行函数的this指向。 但是这里调用foo()的还是window,所以输出了2。
题目7
var obj = {
age: 18,
foo: function (func) {
func();
arguments[0]();
}
};
var age = 10;
function temp() {
console.log(this.age);
}
【解析】
1、把函数temp当成参数时,会发生隐式丢失,temp的this会指向window。所以输出了10。
2、arguments是个类数组,这里调用函数试会发生隐式绑定,所以函数的this指向了调用它的arguments。
所以输出了undefined
输出
10
undefined
五、原型 & 继承
每个实例对象都有一个原型指针__proto__,指向构造函数原型prototype,并从中继承属性和方法。
访问对象的属性时,会在对象的原型__proto__去找,也就是去构造函数的原型prototype去找。
原型可能又有自己的原型,它的原型指针指向它的构造函数的原型prototype。
这样一层一层,最终指向null,这个查找的链条被称作原型链。
题目1
function Person(name) {
this.name = name
}
var p2 = new Person('king');
console.log(p2.__proto__) //Person.prototype
console.log(p2.__proto__.__proto__) //Object.prototype
console.log(p2.__proto__.__proto__.__proto__) // null
console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.constructor)//Person
console.log(p2.prototype)//undefined p2是实例,没有prototype属性
console.log(Person.constructor)//Function 一个空函数
console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性
console.log(Person.prototype.constructor)//Person
console.log(Person.prototype.__proto__)// Object.prototype
console.log(Person.__proto__) //Function.prototype
console.log(Function.prototype.__proto__)//Object.prototype
console.log(Function.__proto__)//Function.prototype
console.log(Object.__proto__)//Function.prototype
console.log(Object.prototype.__proto__)//null
【解析】
首先屡清楚p2、Person、Function、Object的关系。
在JS里,万物皆对象。方法
Function也是对象。记住2句重点:1、所有函数的默认原型都是
Object的实例。2、所有对象都是
Function的实例。包括Object和Function。事实上,相当于function Object和function Function
题目2
var F = function() {};
Object.prototype.a = function() {
console.log('a');
};
Function.prototype.b = function() {
console.log('b');
}
var f = new F();
f.a();
f.b();
F.a();
F.b()
根据题目1的图和关系说明,我们可以得出下面的分析。
1、F是构造函数,构造函数是Function的实例,构造函数又是对象Object的实例。原型链关系如下:
F.__proto__ === Function.prototype.__proto__ === Object.prototype
所以F.b()沿着原型链能找到找到Object.prototype.a上的属性。
2、F是构造函数,构造函数是Function的实例,F的原型指针指向构造函数Function,并继承其属性和方法,所以可以访问到Function.prototype.b的方法。
六、JS异步
setImmediate(() => {
console.log('immediate')
})
async function async1 () {
console.log('async1 start') // 同步
await async2() // 同步
console.log('async1 end') // await 后面的内容,异步(微任务),相当于 callback 函数里的内容
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1() // 同步
new Promise (function (resolve) {
console.log('promise1') // 同步
resolve()
}).then (function () {
console.log('promise2')
})
process.nextTick(() => {
console.log('nextTick')
})
console.log('script end')
【解析】
这道题考察nodejs异步。
首先,先执行同步代码。
1、script start
2、注意new Promise的代码await代码也是同步执行的。所以接着依次输出:
async1 start,async2,promise1,script end';
3、接着执行异步任务,先执行微任务:
在Nodejs中,微任务最先执行的是 process.NextTick,
所以接下来输出 nextTick。然后是其他类型的微任务:async1 end;
4、接着执行宏任务,Nodejs的宏任务类型比较多,不可能像浏览器一样放在把到期的宏任务都放在异步队列里面。 Nodejs的宏任务执行分为六个阶段,
1. timer阶段:执行定时器setTimeout和setInterval的回调。
2. I/O callbacks: 处理流、TCP的错误回调。
3.idle,prepare: 闲置阶段,node内部使用。
4. poll 阶段:执行poll中的I/O队列,检查定时器是否到期
5. check:存放setTmmediate的回调
6. close callback: 执行关闭回调
所以,宏任务 setTimeout的执行时机比setTmmediate早。
所以接下来依次输出
setTimeout和immediate
你做对了吗? : )