JS题大赏

105 阅读7分钟
  1. 变量提升
"use strict"
a = 1
var a = 2
console.log(window.a) // 2
console.log(a) // 2

/*
 var声明会把变量提升到当前作用域的顶部,所以a=1不会报错
 全局作用域下使用var声明变量,该变量会变成window的一个属性
 以上2点与是否在严格模式下无关
*/
  1. 变量在未定义前就使用,会形成暂时性性死区
function sum(a = b, b =1) {
    console.info(a, b)
}
sum() // Cannot access 'b' before initialization

// 先执行a = b, 但是b还未定义不能使用
  1. 变量提升
var name = 'zs'
;(function() {
    if(typeof name === 'undefined') {
        var name = 'tom'
        console.info('Goodbye ' + name)
    } else {
        console.info('hi ' + name)
    }
})()
// 'Goodbye zs'

/*
 变量提升,var声明变量时会把变量自动提升到当前作用域的顶部
*/
  1. 变量提升
var a = 1
function a(){}
console.log(a)

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

function b(){}
var b
console.log(b)
// 1  b函数本身 b函数本身

/*
    函数声明和var声明,两者都会发生提升,但是函数会优先提升,如果变量和函数同名,变量的提升就会忽略
*/
  1. 变量提升
console.info(a)
var a = 1
var getA = function() {
    a = 2
}
function getA() {
    a = 3
}
console.info(a)
getA()
console.info(a)
// undefined  1  2

/*
 变量提升将变量a 提升到顶部
 打印变量a 为 undefined
 
 函数声明和函数表达式的区别,函数声明会有提升作用,在代码执行前就把函数提升到顶部,在执行上下文中生成函数定义,第二个getA会提升到顶部,然后是var声明getA提升,因为getA函数已经被声明,所以不需要再声明。
 
 变量a 赋值为 1
 打印变量a 为 1
 
 变量getA赋值 新函数 function() {a=2}
 执行getA
 打印变量a 为 2
*/
  1. 作用域链连等操作符从右向左执行
let a = b =10
;(function () {
    let a = b = 20
})()
console.info(a) // 10
console.info(b) // 20

// 连等操作是从右向左执行的,相当于 全局b = 10, 全局a = 全局b,b没有声明就赋值了,会创建一个全局变量。
// 函数内执行同外部一样,b没有声明,直接对b赋值,因为作用域链,会一层一层向上查找,找到全局b,所以全局b被修改成20
  1. 作用域链
var i = 1
function b() {
  console.log(i)
}
function a() {
  var i = 2
  b()
}
a() // console.info(1)

/*
    作用域是一套变量的查找规则,每个函数在执行时都会创建一个执行上下文,其中会关联一个变量对象,也就是它的作用域,上面保存着函数能访问的所有变量,另外上下文中的代码在执行时还会创建一个作用域链
    如果某个标识符在当前作用域中没有找到,会沿着外层作用域链继续查找,直到最顶端的全局作用域,因为js是词法作用域,在写代码阶段就作用域已经确定,在函数定义的时候确定的,而不是执行的时候。
    因为b函数是在全局作用域中定义的,虽然在a函数中调用,但是它只能访问到全局的作用域,访问不到a函数作用域。
*/
  1. 作用域链
var scope = 'global scope'
function a(){
  function b(){ 
    console.log(scope)
  }
  return b
  var scope = 'local scope'
}
a()() // undefined

/*
    变量scope会提升到函数作用域顶部,b的上层作用域是a函数,自身没有scope会去上层作用域查找
*/
  1. 对象通过指针引用,. 运算符优先级高, 连等操作符从右向左执行
var a = {n: 1}
var b = a
a.x = a = {n:2}
console.info(a.x) // undefined
console.info(b.x) // {n: 2}

/*
  对象是通过指针指向堆内存中的一个对象
  a = {n: 1}, 变量a的指针指向 初始对象{n:1}
  b = a, 变量b的指针也指向 初始对象{n:1}
  因为 . 运算符级别高,先执行 a.x  初始对象{n: 1, x: undefined}
  a = {n: 2}, 变量a的指针指向 新对象{n:2}
  a.x = a, 因为a.x最早执行过,所以相当于 初始对象{n: 1, x: undefined} 的 x 指向 新对象
  原理 ({n: 1, x: undefined}).x = b.x = a = {n: 2}
*/
  1. 数组遍历方法,不同ES版本,对undefined处理方法不同
var arr = [012]
arr[10] = 10
console.log(arr.filter(function (x) {
  return x === undefined
}))
// 结果[]

/*
 arr[10] = 10, 那么索引3-9位置上都是undefined,arr[3]打印也是undefined,
 但是这里涉及ECMAScript版本不同对应方法行为不同的问题,ES6之前遍历方法都会跳过未赋值过的位置,也就是空位。但是ES6新增的for..of方法不会跳过
*/
  1. + 符号的原理
console.info(1 + NaN) // NaN
console.info(1 + '3') // '13'
console.info(1 + true) // 2  (true转成1)
console.info(1 + false) // 1  (false转成0)
console.info(1 + undefined) // NaN  (undefined转成NaN) 
console.info(1 + null) // 1  (null转成0)
console.info(1 + {}) // '1[object Object]'
console.info(1 + []) // 1 (数组转成0)
console.info({} + []) // '[object Object]'

/*
 考察 + 号,左侧都有值的行为
 1.如果一个操作数是字符串,那么把另一个操作数转成字符串执行连接
 2.如果一个操作数是对象,那么调用对象的valueOf方法转成原始值,如果没有该方法或调用后是非原始值,则调用toString方法
 3.其他情况下,两个操作符都会被转成数字执行加法操作
*/
  1. 对象的key如果是对象,会被转成[object Object]
var a = {}
    b = {key: 'b'}
    c = {key: 'c'}
a[b] = 123
a[c] = 456
console.info(a[b]) // 456
console.info(a[c]) // 456
console.info(a) // {[object Object]: 456}

/*
  对象的key如果是对象,会转成[object Object]
  要使用对象作为key,可以设置 map 类型数据
  var a = new Map()
*/
  1. this指向
var out = 25
var inner = {
  out20,
  funcfunction () {
    var out = 30
    return this.out
  }
};
console.log((inner.func, inner.func)()) // 25
console.log(inner.func()) // 20
console.log((inner.func)()) // 20
console.log((inner.func = inner.func)()) // 25

/*
  1逗号操作符会返回表达式中的最后一个值,这里为inner.func对应的函数,注意是函数本身,然后执行该函数,该函数并不是通过对象的方法调用,而是在全局环境下调用,所以this指向window,打印出来的当然是window下的out
  2这个显然是以对象的方法调用,那么this指向该对象
  3实际上(inner.func)和inner.func是完全相等的,还是作为对象的方法调用
  4赋值表达式和逗号表达式相似,都是返回的值本身,所以也相对于在全局环境下调用函数
*/
  1. this指向
function fn (){
  console.log(this)
}
var arr = [fn]
arr[0]()
// 打印arr本身

/*
  数组调用了函数,所以this指向数组。类似于obj['fn']()
*/
  1. this指向
var obj = {
  name'abc',
  fn() => {
    console.log(this.name)
  }
};
obj.name = 'bcd'
obj.fn() // ''

/*
  箭头函数执行的时候上下问时不会绑定this的,它的this取决于外层的this
  函数执行的时候外层时全局作用域,this指向window。window对象有name属性为空
*/
  1. 解构赋值顺序
let {a, b, c} = {c: 3, b: 2, a: 1}
console.info(a, b, c) // 1, 2, 3

/*
  数组解构赋值是按位置对应的,而对象只要变量与属性同名,顺序随意
*/
  1. Object.assgin()合并对象
console.info(Object.assgin([1, 2, 3], [4, 5])) // 4, 5, 3

/*
  Object.assgin()合并对象会按对应key合并,合并数组会按下角标合并,将数组的0,1替换
*/
  1. typeof 类型判断 js关键字区分大小写 运算符优先级
console.info(typeof undefined == typeof NULL) // true
console.info(typeof function(){} == typeof class {}) // true

/*
  js关键字区分大小写,第一行是大写NULL,不是小写null。所以就是未声明的普通变量
  class是ES6新增语法糖,本质还是函数
*/

var count = 0
console.info(typeof count === 'number') // true
console.info(!!typeof count == 'number') // false

/*
  考查运算符优先级,逻辑 !! 的优先级比全等 === 高,先执行!!typeof count,结果为true,然后执行 true === 'number' 结果为false
*/
  1. 变量提升 构造函数 原型连
// 构造函数
function Foo() {
    getName = function() {console.info(1)}
    return this
}
// 函数方法
Foo.getName = function() {console.info(2)}
// 原型对象方法
Foo.prototype.getName = function() {console.info(3)}
// 函数表达式
var getName = function() {console.info(4)}
// 函数声明
function getName() {console.info(5)}

Foo.getName()  // 2
getName() // 4
Foo().getName() // 1
getName() // 1
new Foo.getName() // 2
new Foo().getName() // 3
new new Foo().getName() // 3
// 2 4 1 1 2 3 3

/*
  1 执行Foo函数的静态方法,打印2
  2 执行getName,因为变量提升以及重新赋值,打印4
  3 执行Foo(), 重新赋值全局getName函数,并返回this值,this指向window。打印1
  4 因为getName被重新赋值,打印1
  5 new操作符是用来调用函数,new Foo.getName() === new (Foo.getName)(), 打印2
  6 因为运算符 (.) 的优先级和 new 一样高,所以从左向右执行。new Foo.getName() = (new Foo()).getName()
      先创建实例,再通过实例调用原型对象的方法, 打印3
  7 new new Foo().getName() = new((new Foo()).getName)()
      因为 (.) 与 new级别一样高,所以先 new Foo(), 再执行 (new Foo()).getName ,最后 new ((new Foo()).getName)()
*/