预测执行结果

发布于 2023-01-06  125 次阅读


下面代码输出的结果?

for(var i = 0; i < 3; i++){
  setTimeout(function(){
      console.log(i);
  },0);
};

答案:3,3,3

如何才能打印出 0 1 2

解决方法:

for(let i = 0; i < 3; i++){
  setTimeout(function(){
      console.log(i);
  },0);
};
// 0 1 2
for (var i = 0; i < 3; i++) {
  (function(i) {
    setTimeout(function () {
      console.log(i);
    }, 0, i)
  })(i)
};
// 0 1 2

下面代码打印结果是什么?为什么?

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

答案:

  • 非严格模式:[Function b]
  • 严格模式:Uncaught TypeError: Assignment to constant variable

解析:

var b = 10;
(function b() {
   // 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
   // IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
  // (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等)
    b = 20;
    console.log(b); // [Function b]
    console.log(window.b); // 10,不是20
})();
// 严格模式下能看到错误:Uncaught TypeError: Assignment to constant variable

其他情况例子:

// 有window:
var b = 10;
(function b() {
  window.b = 20;
  console.log(b); // [Function b]
  console.log(window.b); // 20是必然的
})();
// 有var:
var b = 10;
(function b() {
  var b = 20; // IIFE内部变量
  console.log(b); // 20
  console.log(window.b); // 10
})();

解析来源 ←

输出以下代码执行的结果并解释为什么

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

结果:

解析参考:

结果最后变为一个稀疏数组[,,1,2]。 因为在对象中加入splice属性方法,和length属性后。这个对象变成一个类数组。属性名称可转换为数字时,会映射成为索引下标。所以对象实际可这样描述:[,,3,4],然后继续执行length 属性复制,length 原来值为4,length:2 语句将数组截断成为长度为2的数组。此时数组为[,,]。接着push(1)、push (2)。数组追加两个值,[,,1,2]。小菜看法,有错望指正!!!

参考来源:

下面代码中 a 在什么情况下会打印 1?

var a = ?;
if (a == 1 && a == 2 && a == 3) {
 	console.log(1);
}

解析:因为 == 会进行隐式类型转换 所以我们重写 toString 方法就可以了

答案:

var a = {
  i: 1,
  toString() {
  	return a.i++;
  }
}

if(a == 1 && a == 2 && a == 3){
 	console.log(1);
}

输出以下代码的执行结果并解释为什么

var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a.x);
console.log(b.x);

答案:

a.x   // --> undefined
b.x   // --> {n: 2}

解析:

var a = { n: 1 }; // a保持对{n:1}对象的引用
var b = a; // b保持对{n:1}对象的引用
a.x = a = { n: 2 }; // a的引用被改变

a.x 	// --> undefined
b.x 	// --> {n: 2}

// 画图
var a = { n: 1 }; // a 是个对象
var b = a;
a.x = a = { n: 2 };
console.log(a.x); // undefined
console.log(b.x); // { n: 2 }

/**
 * 栈             堆
 * a:002            001: { n: 1, x: 002 }
 * b:001            002: { n: 2 }
 */
  1. . 运算符优先,a.x 此时保持对 {n: 1} 的引用,也就是 b 也保持对 {n: 1} 的引用,于是 {n: 1} => {n: 1, x: undefined},此时 ab 还是对原来对象的引用,只不过原来对象增加了 x 属性
  2. = 从右往左,a = {n: 2},此时a的引用已经变成了 {n: 2} 这个对象
  3. a.x = a,此时 a.x 是保持对 { n: 1, x: undefined } 中的 x 引用,也就是 b.x ,于是 { n: 1, x: undefined } => {n: 1, x: { n: 2}},即 b.x = { n: 2 }题目来源:Daily-Interview-Question 第53题

输出以下代码运行结果

// example 1
var a = {}, b = '123', c = 123;
a[b] = 'b';
a[c] = 'c';
console.log(a[b]);
---------------------
// example 2
var a = {}, b = Symbol('123'), c = Symbol('123');
a[b] = 'b';
a[c] = 'c';
console.log(a[b]);
---------------------
// example 3
var a = {}, b = { key:'123' }, c = { key:'456' };
a[b] = 'b';
a[c] = 'c';
console.log(a[b]);

分析:这题考察的是对象的键名的转换。

  • 对象的键名只能是字符串和 Symbol 类型。
  • 其他类型的键名会被转换成字符串类型。
  • 对象转字符串默认会调用 toString 方法。

解答:

// example 1
var a={}, b='123', c=123;
a[b]='b';
// c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。
a[c]='c';
// 输出 c
console.log(a[b]);
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
// b 是 Symbol 类型,不需要转换。
a[b]='b';
// c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的,所以不会覆盖掉 b。
a[c]='c';
// 输出 b
console.log(a[b]);
// example 3
var a={}, b={key:'123'}, c={key:'456'};
// b 不是字符串也不是 Symbol 类型,需要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。
a[b]='b';
// c 不是字符串也不是 Symbol 类型,需要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉。
a[c]='c';
// 输出 c
console.log(a[b]);

题目来源:Daily-Interview-Question 第76题

以下代码中,当 foo 是哪些值的时候,会执行 alert ?

var foo = 'false';
    foo = 'true';
    foo = 1;
    foo = 0;
    foo = -1;
    foo = "";
    foo = undefined;
    foo = null;
    foo = NaN;
    foo = false;
    foo = [];
    foo = {};

if (!foo) { alert('foo') }

答案:

  • 分析:
  • 考点:哪些值 Boolean 后为 false
  • undefined、null、''、0、NaN、false 六个值为假,其余都为真
var foo = 'false'; // 不行 	非空字符串的字符串,Boolean() 后为真
    foo = 'true'; // 不行
    foo = 1; // 不行
    foo = 0; // 行
    foo = -1; // 不行				只有数字 0 Boolean 后才为 false ,其余数字都为 true
    foo = ""; // 行
    foo = undefined; // 行
    foo = null; // 行
    foo = NaN; // 行
    foo = false; // 行
    foo = []; // 不行
    foo = {}; // 不行

以下代码运行输出结果是什么

var num = 100;
var fn = function() {
  var num = ++num;
  console.log(num);
}
fn();

解析:

函数执行时,寻找变量和形参提升,发现只有 num
所以 fn 的 ao 中 num: undefined
接着 num = ++num 也就是 num = ++undefined;
++undefined 为 NaN
所以最终打印 NaN

下面打印是什么

var obj1 = Object.create(null);
obj1.a = 1;

var obj2 = Object.create(obj1);
obj2.b = 2;

console.log(obj2.__proto__);

原型存在在原型链上,但现在链不存在了(就是最终到达 Object.prototype 的这个链儿 在 obj1 这断掉了)

  • 所以只要原型链断掉,往回倒得所有对象就都无法通过 自己.__proto__ 找到原型,为 undefined(也就是指针不指向任何原型了,就是 undefined 了)
  • __proto__ 是原型链的指针,原型链都被破坏了,这个指针也断了
  • __proto__ 是 Object 的 属性

打印预测

var p1 = Promise.resolve( 1 );
var p2 = Promise.resolve( p1 );
var p3 = new Promise(function(resolve, reject){
  resolve(1);
});
var p4 = new Promise(function(resolve, reject){
  resolve(p1);
});

console.log(p1 === p2);
console.log(p1 === p3);
console.log(p1 === p4);
console.log(p3 === p4);

p4.then(function(value){
  console.log('p4=' + value);
});

p2.then(function(value){
  console.log('p2=' + value);
})

p1.then(function(value){
  console.log('p1=' + value);
})
// 答案在下

// true
// false
// false
// false
// p2=1
// p1=1
// p4=1

Promise.resolve(...)可以接收一个值或者是一个Promise对象作为参数。
当参数是普通值时,它返回一个resolved状态的Promise对象,对象的值就是这个参数;
当参数是一个Promise对象时,它直接返回这个Promise参数。因此,p1 === p2。
但通过new的方式创建的Promise对象都是一个新的对象,因此后面的三个比较结果都是false。
另外,为什么p4的then最先调用,但在控制台上是最后输出结果的呢?
因为p4的resolve中接收的参数是一个Promise对象p1,resolve会对p1”拆箱“,获取p1的状态和值,
但这个过程是异步的

预测结果

Array.apply(null, { length: 2 })

解析:

打印 [undefined, undefined]

Array.apply(null, { length: 2 })
// 等价于
Array.apply(null, [undefined, undefined])
// 等价于
Array(undefined, undefined)
// 等价于
new Array(undefined, undefined)

Array.apply(null, { length: 2 }) 是什么意思

预测打印结果 - 参数默认值

var x = 1;

function test(x, y = function() {
  x = 3;
  console.log(x);
}) {
  console.log(x); // x 未赋值,打印undefined
  var x = 2; // 把 x 改为 2
  y(); // 执行y函数,改的是参数作用域中的x,打印 3
  console.log(x); // 函数内部 x 现在为 2
}

test();
console.log(x); // 全局x,打印 1
  • 参数赋值默认值后,参数就形成了独立作用域

var x = 2; 删掉之后:

var x = 1;

function test(x, y = function() {
  x = 3;
  console.log(x);
}) {
  console.log(x); // 形参 x 此刻 undefined
  y(); // 依然改的是参数作用域中 x ,改后为 3
  console.log(x); // 局部作用域中没有声明 x ,在参数作用域中找,找到了 3
}

test();
console.log(x); // 打印全局x 1

继续改造,把形参 x 改为 a

var x = 1;

function test(a, y = function() {
  x = 3;
  console.log(x);
}) {
  console.log(x); // 打印形参内部 x,此刻为 undefined
  var x = 2;
  y(); // 参数作用域中没有 x ,外层找,全局有 x ,改全局 x 为 3 ,打印 3
  console.log(x); // 局部作用域中 x 已经改为了 2,打印 2
}

test();
console.log(x); // 全局x已改为3

继续改造,把形参 a 改为 x = 4

var x = 1;

function test(x = 4, y = function() {
  x = 3;
  console.log(x);
}) {
  console.log(x); // 预编译阶段,x先为undefined,然后实参赋值给形参,没有实参,用4赋值,所以为4
  var x = 2;
  y(); // 更改参数作用域x为3
  console.log(x); // 局部作用域中x,打印2
}

test();
console.log(x); // 全局还是1

继续改造,参数 y 函数中我不更改 x 值了,直接打印 x:

var x = 1;

function test(x = 4, y = function() {
  console.log(x); // 参数作用域中x还是4
}) {
  console.log(x); // 预编译阶段x为默认值4
  var x = 2; // 把局部作用域x改为2
  y();
  console.log(x); // 局部作用域中x改为了2
}

test();
console.log(x);// 全局1

再来改造,我们把原先参数 y 函数提到外边去:

var x = 1;
function yy() {
	x = 3;
  console.log(x);
}

function test(x = 4, y = yy) {
	console.log(x); // 预编译阶段x为默认值4
  var x = 2; // 局部作用域x改为2
  y(); // 相当于定义在全局的yy函数引用执行 -> 全局的x 改为 3
  console.log(x);
}

test();
console.log(x); // 全局改为3了

this 指向

function test() {
	console.log(foo);
  var foo = 2;
  console.log(foo);
  console.log(a);
}

test();

解析:

AO: {
	foo: undefined -> 2
}

// 1. 变量声明提升 foo: undefined
// 2. 执行test
// 3. 第一行打印 foo 为 undefined
// 4. foo 赋值为 2
// 5. 再次打印 foo , 此刻为 2
// 6. 打印变量 a ,而 test 的 AO 中不存在 a ,所以报错 a is not defined

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

解析:

AO {
	test: undefined -> fn
}

打印1

var name = '222';
var a = {
	name: '111',
  say: function() {
    console.log(this);
  	console.log(this.name);
  }
}
var fun = a.say;
fun(); // ①
a.say(); // ②
var b = {
	name: '333',
  say: function(fn) {
    console.log(fn, this)
  	fn();
  }
}
b.say(a.say); // ③
b.say = a.say;
b.say(); // ④

解析:

fun()
把 a.say 这个函数赋值给 fun
然后 fun(); 此刻默认调用者是 window, so 此刻 this 指向 window
所以 1 处打印 window, '222'

a.say()
调用 say 方法的对象是 a ,所以 this 指向为 a
所以 2 处打印 a对象, '111'

b.say(a.say)
把 a.say 这个函数扔进 b.say() 参数中
此刻 this 为 b
所以 3 处 先打印 a.say 函数,b对象;
再执行 fn() 此刻没有 this.fn(), 所以是 window 调用 a.say 函数
所以最后打印 window, '222'

b.say()
在此之前把 a.say 函数赋值(覆盖)给 b.say 函数
b.say() 调用者是 b,所以 4 处打印 b对象, '333'

function test() {
	var marty = {
  	name: 'marty',
    printName: function(){
    	console.log(this.name);
    }
  }
  
  var test1 = {
  	name: 'test1'
  }
  
  var test2 = {
  	name: 'test2'
  }
  
  var test3 = {
  	name: 'test3'
  }
  
  test3.printName = marty.printName;
  marty.printName.call(test1);
  marty.printName.apply(test2);
  marty.printName();
  test3.printName();
}

test();

解析:

marty.printName.call(test1); 相当于把 marty.printName 方法借给了 test1
所以执行时 this 指向改变为了 test1 而不是原本的 marty
所以   marty.printName.call(test1); 打印 'test1'

同理 marty.printName.apply(test2); 打印 'test2'

marty.printName(); 调用者是 marty 所以打印 'marty'

test3.printName(); 调用者是 test3 ,所以打印 'test3'

经典

function Foo() {
	getName = function() {
  	console.log(1);
  }
 	a = 3;
  return this;
}
Foo.a = 1;
Foo.getName = function() {
	console.log(2, this);
}
Foo.prototype.getName = function() {
	console.log(3, this);
}
var getName = function() {
	console.log(4);
}
function getName() {
	console.log(5);
}
Foo.getName();
getName();
Foo().getName();
console.log(Foo.a, window.a);
new Foo.getName();
new Foo().getName();
new new Foo().getName();
new new Foo();

解析:

// 分析

// 大写 F ,表明把此函数当做构造函数看待
function Foo() {
  // 不执行Foo,下边对 getName和a的赋值不执行
  // getName和a前都没有var,而且参数中也没有同名变量,所以改的是全局window下的属性getName和a
	getName = function() {
  	console.log(1);
  }
 	a = 3;
  return this;
}
// 给 Foo 添加一个静态属性 a,此属性在实例化 Foo 时,不存在于实例化对象的属性中
Foo.a = 1;
// 给 Foo 添加一个静态方法 getName,此方法不存在于实例化对象的属性中
Foo.getName = function() {
	console.log(2, this);
}
// 扩展函数原型上的方法
Foo.prototype.getName = function() {
	console.log(3, this);
}
// 给全局变量 getName 赋值一个新函数
var getName = function() {
	console.log(4);
}
// 函数声明
function getName() {
	console.log(5);
}

Foo.getName();
getName();
Foo().getName();
console.log(Foo.a, window.a);
new Foo.getName();
new Foo().getName();
new new Foo().getName();

// 预编译
GO: {
	Foo: Foo构造函数,
  getName: undefined (var变量提升) -> 
    			 getName() { log5 } (函数声明覆盖了)
}

// 开始执行:

// 1. Foo.a = 1; 给 Foo 构造函数添加一个静态属性 a = 1
// 2. Foo.getName = function(){console.log(2)},给Foo构造函数添加一个静态方法getName
// 3. Foo.prototype.getName=function() {console.log(3);}, 扩展Foo原型方法getName
// 4. var getName = function() {console.log(4);} 覆盖全局属性getName为新函数
GO: {
	Foo: Foo构造函数,
  getName: undefined (var变量提升) -> 
    			 getName() { log5 } (函数声明覆盖了) ->
    			 fn() { log4 } (表达式覆盖)
}

// ------------------

// 5. Foo.getName(): 执行 Foo 函数静态方法, 打印 2, 打印的 this 为此刻 getName 调用者 Foo
// 6. getName(): 执行全局 getName 打印 4
// 7. Foo().getName(): Foo()隐式window调用,this是window 执行 Foo 形参 AO,修改 GO
AO: {
	啥都没有,没有变量提升和函数声明提升,也没有形参提升
}
GO: {
	Foo: Foo构造函数,
  getName: undefined (var变量提升) -> 
    			 getName() { log5 } (函数声明覆盖了) ->
    			 getName() { log1 },
  a: 3
}
// 调用 Foo() 后修改了全局 window 的 getName,最后 return this 其实 return 的是 window
// 所以 Foo().getName() 相当于 window.getName(),所以打印 1

// 8. console.log(Foo.a, window.a): 打印 Foo 静态属性a 为 1,window下a为3
// 9. new Foo.getName(): new 了 Foo 的静态方法 getName,打印 2,
// 打印的this是一个新对象,它不是 window 也不是 Foo 本身

// 10. new Foo().getName(): 相当于 (new Foo()).getName() 即先实例化 Foo,再调用
// 实例化对象foo的getName方法,即调用 Foo.prototype.getName 打印 3,
// 打印的 this 为新的 Foo 实例化对象

// 11. new new Foo().getName(): 可以看做 new ( (new Foo()).getName() ), 也即相当于
// new Foo.prototype.getName() ,也就是把 Foo 原型上的 getName 当做构造函数来new ,
// 还是打印 3, 打印的 this 为一个新对象

// 12. new new Foo(); 报错 , 先执行 new Foo() 得到一个实例化对象,
// 再用 new 去 new 一个实例化对象,就报错了

总结

  • 点(.)的优先级高于new
  • new Foo.getName(). 的优先级高于 new
  • new Foo().getName()new Foo() 优先级更高
  • 相当于 先 new Foo(),再调用 new 出来的实例对象的 getName 方法

来源

https://www.bilibili.com/video/BV1WX4y1K7vS/?zw&vd_source=77cc082ff7e1bc29048aed56270236b3

闭包

function Test(a, b, c) {
  var d = 0;
  this.a = a;
  this.b = b;
  this.c = c;

  function e() {
    d++;
    console.log(d);
  }

  this.f = e;
}

var test1 = new Test();
test1.f();                                                                                       // 1
test1.f();                                                                                       // 2

var test2 = new Test();
test2.f();                                                                                       // 1

typeof

var test = function a() {
	return 'a';
}
console.log(typeof(a));                                                                               // "undefined"

函数表达式

var test = function a() {
	console.log({
  	'inner test.name': test.name,
    'inner a': a
  })
};

test();

console.log({
	'outer test.name': test.name
});
console.log({
  'outer a': a
})

// 函数表达式,函数外部忽略函数名,内部可以访问

下面的代码打印什么内容,为什么?

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

答案:

严格模式:

报错:Uncaught TypeError: Assignment to constant variable.

非严格模式:

打印:function b() { b = 20; console.log(b); }

解析:

var b = 10;
(function b() {
   // 内部作用域,会先去查找作用域内部是否已有变量 b 的声明,有就直接赋值 20
   // 发现有具名函数 function b() {},就尝试给其赋值
   // 但由于IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
    b = 20;
    console.log(b); // [Function b]
    console.log(window.b); // 10,不是 20
})();

IIFE 内部机制:

当遇到具名的函数表达式的时,会创建一个辅助的特定对象,将函数表达式的名称作为唯一的 key,用来存储函数表达式的名称,然后添加到函数的作用域链中,该值只读,并且不可以被删除,所以不能对该值进行操作。

所以,在严格模式下,一个不可修改的常量被修改之后就会报 TypeError: Assignment to constant variable

分析非严格模式的输出,在非严格模式下会静默失败,所以不报错。针对如上的分析之后,会输出立即执行的具名函数b本身。

以下代码输出的结果是 ?

Object.prototype.a = 10;
var s = Symbol();
var obj = {
    [s]: 20,
    b: 30
}
Object.defineProperty(obj, 'c', {
    enumerable: true,
    value: 40
})
for(let val in obj) {
    console.log(val)
}

以上输出的结果为:b c a

for...in语句以任意顺序遍历一个对象的可枚举属性(除 Symbol 类型的属性)

最后更新于 2023-07-20