JavaScript基础知识梳理

? 内容速览 ?

普通函数和箭头函数的this
原始数据类型及其判断和转化方法
深浅拷贝及实现
JS事件模型
常见的高阶函数
普通函数和箭头函数的this
还是一道经典题目,下面的这段代码的输出是什么?(为了方便解释,输出放在了注释中)

function fn() {
  console.log(this); // 1. {a: 100}
  var arr = [1, 2, 3];

  (function() {
    console.log(this); // 2. Window
  })();

  // 普通 JS
  arr.map(function(item) {
    console.log(this); // 3. Window
    return item + 1;
  });
  // 箭头函数
  let brr = arr.map(item => {
    console.log("es6", this); // 4. {a: 100}
    return item + 1;
  });
}
fn.call({ a: 100 });
其实诀窍很简单,常见的基本是3种情况:es5普通函数、es6的箭头函数以及通过bind改变过上下文返回的新函数。

① es5普通函数:

函数被直接调用,上下文一定是window
函数作为对象属性被调用,例如:obj.foo(),上下文就是对象本身obj
通过new调用,this绑定在返回的实例上
② es6箭头函数: 它本身没有this,会沿着作用域向上寻找,直到global / window。请看下面的这段代码:

function run() {
  const inner = () => {
    return () => {
      console.log(this.a)
    }
  }

  inner()()
}

run.bind({a: 1})() // Output: 1
③ bind绑定上下文返回的新函数:就是被第一个bind绑定的上下文,而且bind对“箭头函数”无效。请看下面的这段代码:

function run() {
  console.log(this.a)
}

run.bind({a: 1})() // output: 1

// 多次bind,上下文由第一个bind的上下文决定
run
  .bind({a: 2})
  .bind({a: 1})
  () // output: 2
最后,再说说这几种方法的优先级:new > bind > 对象调用 > 直接调用

至此,这道题目的输出就说可以解释明白了。

原始数据类型和判断方法
题目:JS中的原始数据类型?

ECMAScript 中定义了 7 种原始类型:

Boolean
String
Number
Null
Undefined
Symbol(新定义)
BigInt(新定义)
注意:原始类型不包含Object和Function

题目:常用的判断方法?

在进行判断的时候有typeof、instanceof。对于数组的判断,使用Array.isArray():

typeof:

typeof基本都可以正确判断数据类型
typeof null和typeof [1, 2, 3]均返回”object”
ES6新增:typeof Symbol()返回”symbol”
instanceof:

专门用于实例和构造函数对应

function Obj(value){ 
    this.value = value; 
}
let obj = new Obj("test");
console.log(obj instanceof Obj); // output: true
判断是否是数组:[1, 2, 3] instanceof Array

Array.isArray():ES6新增,用来判断是否是’Array’。Array.isArray({})返回false。

原始类型转化
当我们对一个“对象”进行数学运算操作时候,会涉及到对象 => 基础数据类型的转化问题。

事实上,当一个对象执行例如加法操作的时候,如果它是原始类型,那么就不需要转换。否则,将遵循以下规则:

调用实例的valueOf()方法,如果有返回的是基础类型,停止下面的过程;否则继续
调用实例的toString()方法,如果有返回的是基础类型,停止下面的过程;否则继续
都没返回原始类型,就会报错
请看下面的测试代码:

let a = {
  toString: function() {
    return 'a'
  }
}

let b = {
  valueOf: function() {
    return 100
  },
  toString: function() {
    return 'b'
  }
}

let c = Object.create(null) // 创建一个空对象

console.log(a + '123') // output: a123
console.log(b + 1) // output: 101
console.log(c + '123') // 报错
除了valueOf和toString,es6还提供了Symbol.toPrimitive供对象向原始类型转化,并且它的优先级最高!!稍微改造下上面的代码:


let b = {
  valueOf: function() {
    return 100
  },
  toString: function() {
    return 'b'
  },
  [Symbol.toPrimitive]: function() {
    return 10000
  }
}

console.log(b + 1) // output: 10001
最后,其实关于instanceof判断是否是某个对象的实例,es6也提供了Symbol.hasInstance接口,代码如下:

class Even {
  static [Symbol.hasInstance](num) {
    return Number(num) % 2 === 0;
  }
}

const Odd = {
  [Symbol.hasInstance](num) {
    return Number(num) % 2 !== 0;
  }
};

console.log(1 instanceof Even); // output: false
console.log(1 instanceof Odd); // output: true
深拷贝和浅拷贝
题目:实现对象的深拷贝。

在JS中,函数和对象都是浅拷贝(地址引用);其他的,例如布尔值、数字等基础数据类型都是深拷贝(值引用)。

值得提醒的是,ES6的Object.assign()和ES7的...解构运算符都是“浅拷贝”。实现深拷贝还是需要自己手动撸“轮子”或者借助第三方库(例如lodash):

手动做一个“完美”的深拷贝函数:https://godbmw.com/passages/2019-03-18-interview-js-code/

借助第三方库:jq的extend(true, result, src1, src2[ ,src3])、lodash的cloneDeep(src)

JSON.parse(JSON.stringify(src)):这种方法有局限性,如果属性值是函数或者一个类的实例的时候,无法正确拷贝

借助HTML5的MessageChannel:这种方法有局限性,当属性值是函数的时候,会报错


<script>
  function deepClone(obj) {
    return new Promise(resolve => {
      const {port1, port2} = new MessageChannel();
      port2.onmessage = ev => resolve(ev.data);
      port1.postMessage(obj);
    });
  }

  const obj = {
    a: 1,
    b: {
      c: [1, 2],
      d: '() => {}'
    }
  };

  deepClone(obj)
    .then(obj2 => {
      obj2.b.c[0] = 100;
      console.log(obj.b.c); // output: [1, 2]
      console.log(obj2.b.c); // output: [100, 2]
    })
</script>
JS事件流
事件冒泡和事件捕获
事件流分为:冒泡和捕获,顺序是先捕获再冒泡。

事件冒泡:子元素的触发事件会一直向父节点传递,一直到根结点停止。此过程中,可以在每个节点捕捉到相关事件。可以通过stopPropagation方法终止冒泡。

事件捕获:和“事件冒泡”相反,从根节点开始执行,一直向子节点传递,直到目标节点。

addEventListener给出了第三个参数同时支持冒泡与捕获:默认是false,事件冒泡;设置为true时,是事件捕获。


<div id="app" style="width: 100vw; background: red;">
  <span id="btn">点我</span>
</div>
<script>
  // 事件捕获:先输出 "外层click事件触发"; 再输出 "内层click事件触发"
  var useCapture = true;
  var btn = document.getElementById("btn");
  btn.addEventListener(
    "click",
    function() {
      console.log("内层click事件触发");
    },
    useCapture
  );

  var app = document.getElementById("app");
  app.onclick = function() {
    console.log("外层click事件触发");
  };
</script>
DOM0级 和 DOM2级
DOM2级:前面说的addEventListener,它定义了DOM事件流,捕获 + 冒泡。

DOM0级:

直接在html标签内绑定on事件
在JS中绑定on系列事件
注意:现在通用DOM2级事件,优点如下:

可以绑定 / 卸载事件
支持事件流
冒泡 + 捕获:相当于每个节点同一个事件,至少2次处理机会
同一类事件,可以绑定多个函数
常见的高阶函数
没什么好说的,跑一下下面的代码就可以理解了:

// map: 生成一个新数组,遍历原数组,
// 将每个元素拿出来做一些变换然后放入到新的数组中
let newArr = [1, 2, 3].map(item => item * 2);
console.log(`New array is ${newArr}`);

// filter: 数组过滤, 根据返回的boolean
// 决定是否添加到数组中
let newArr2 = [1, 2, 4, 6].filter(item => item !== 6);
console.log(`New array2 is ${newArr2}`);

// reduce: 结果汇总为单个返回值
// acc: 累计值; current: 当前item
let arr = [1, 2, 3];
const sum = arr.reduce((acc, current) => acc + current);
const sum2 = arr.reduce((acc, current) => acc + current, 100);
console.log(sum); // 6
console.log(sum2); // 106
目录
1. 普通函数和箭头函数的this
2. 原始数据类型和判断方法
3. 原始类型转化
4. 深拷贝和浅拷贝
5. JS事件流
5.1. 事件冒泡和事件捕获
5.2. DOM0级 和 DOM2级
6. 常见的高阶函数

? 内容速览 ?

实现ES5继承的4种方法
原型和原型链
作用域和作用域链
Event Loop
执行上下文
闭包的理解和分析
ES5继承
题目:ES5中常用继承方法。

方法一:绑定构造函数

缺点:不能继承父类原型方法/属性

function Animal(){
  this.species = '动物'
}

function Cat(){
  // 执行父类的构造方法, 上下文为实例对象
  Animal.apply(this, arguments)
}


/**
 * 测试代码
 */
var cat = new Cat()
console.log(cat.species) // output: 动物
方法二:原型链继承

缺点:无法向父类构造函数中传递参数;子类原型链上定义的方法有先后顺序问题。

注意:js中交换原型链,均需要修复prototype.constructor指向问题。

function Animal(species){
  this.species = species
}
Animal.prototype.func = function(){
  console.log('Animal')
}

function Cat(){}
/**
 * func方法是无效的, 因为后面原型链被重新指向了Animal实例
 */
Cat.prototype.func = function() {
  console.log('Cat')
}

Cat.prototype = new Animal()
Cat.prototype.constructor = Cat // 修复: 将Cat.prototype.constructor重新指向本身

/**
 * 测试代码
 */
var cat = new Cat()
cat.func() // output: Animal
console.log(cat.species) // undefined
方法3:组合继承

结合绑定构造函数和原型链继承2种方式,缺点是:调用了2次父类的构造函数。


function Animal(species){
  this.species = species
}
Animal.prototype.func = function(){
  console.log('Animal')
}

function Cat(){
  Animal.apply(this, arguments)
}

Cat.prototype = new Animal()
Cat.prototype.constructor = Cat 

/**
 * 测试代码
 */
var cat = new Cat('cat')
cat.func() // output: Animal
console.log(cat.species) // output: cat
方法4:寄生组合继承

改进了组合继承的缺点,只需要调用1次父类的构造函数。它是引用类型最理想的继承范式。(引自:《JavaScript高级程序设计》)

/**
 * 寄生组合继承的核心代码
 * @param {Function} sub 子类
 * @param {Function} parent 父类
 */
function inheritPrototype(sub, parent) {
  // 拿到父类的原型
  var prototype = Object.create(parent.prototype) 
  // 改变constructor指向
  prototype.constructor = sub
  // 父类原型赋给子类
  sub.prototype = prototype
}

function Animal(species){
  this.species = species
}
Animal.prototype.func = function(){
  console.log('Animal')
}

function Cat(){
  Animal.apply(this, arguments) // 只调用了1次构造函数
}

inheritPrototype(Cat, Animal)

/**
 * 测试代码
 */

var cat = new Cat('cat')
cat.func() // output: Animal
console.log(cat.species) // output: cat
原型和原型链
所有的引用类型(数组、对象、函数),都有一个__proto__属性,属性值是一个普通的对象
所有的函数,都有一个prototype属性,属性值也是一个普通的对象
所有的引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的prototype属性值
注:ES6的箭头函数没有prototype属性,但是有__proto__属性。

const obj = {};
// 引用类型的 __proto__ 属性值指向它的构造函数的 prototype 属性值
console.log(obj.__proto__ === Object.prototype); // output: true
原型
题目:如何JS中的原型?


// 构造函数
function Foo(name, age) {
    this.name = name
}
Foo.prototype.alertName = function () {
    alert(this.name)
}
// 创建示例
var f = new Foo('zhangsan')
f.printName = function () {
    console.log(this.name)
}
// 测试
f.printName()
f.alertName()
但是执行alertName时发生了什么?这里再记住一个重点 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找,因此f.alertName就会找到Foo.prototype.alertName。

原型链
题目:如何JS中的原型链?

以上一题为基础,如果调用f.toString()。

f试图从__proto__中寻找(即Foo.prototype),还是没找到toString()方法。
继续向上找,从f.__proto__.__proto__中寻找(即Foo.prototype.__proto__中)。因为Foo.prototype就是一个普通对象,因此Foo.prototype.__proto__ = Object.prototype
最终对应到了Object.prototype.toString
这是对深度遍历的过程,寻找的依据就是一个链式结构,所以叫做“原型链”。

作用域和作用域链
题目:如何理解 JS 的作用域和作用域链。

①作用域

ES5有”全局作用域“和”函数作用域“。ES6的let和const使得JS用了”块级作用域“。

为了解决ES5的全局冲突,一般都是闭包编写:(function(){ ... })()。将变量封装到函数作用域。

②作用域链

当前作用域没有找到定义,继续向父级作用域寻找,直至全局作用域。这种层级关系,就是作用域链。

Event Loop
单线程
题目:讲解下面代码的执行过程和结果。


var a = true;
setTimeout(function(){
    a = false;
}, 100)
while(a){
    console.log('while执行了')
}
这段代码会一直执行并且输出”while…”。JS是单线程的,先跑执行栈里的同步任务,然后再跑任务队列的异步任务。

执行栈和任务队列
题目:说一下JS的Event Loop。

简单总结如下:

JS是单线程的,其上面的所有任务都是在两个地方执行:执行栈和任务队列。前者是存放同步任务;后者是异步任务有结果后,就在其中放入一个事件。
当执行栈的任务都执行完了(栈空),js会读取任务队列,并将可以执行的任务从任务队列丢到执行栈中执行。
这个过程是循环进行,所以称作Loop。
执行上下文
题目:解释下“全局执行上下文“和“函数执行上下文”。

①全局执行上下文

解析JS时候,创建一个 全局执行上下文 环境。把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。未赋值的变量就是undefined。

下面这段代码输出:undefined;而不是抛出Error。因为在解析JS的时候,变量a已经存入了全局执行上下文中了。


console.log(a);
var a = 1;
②函数执行上下文

和全局执行上下文差不多,但是多了this和arguments和参数。

在JS中,this是关键字,它作为内置变量,其值是在执行的时候确定(不是定义的时候确定)。

闭包的理解和分析
题目:解释下js的闭包

直接上MDN的解释:闭包是函数和声明该函数的词法环境的组合。

而在JavaScript中,函数是被作为一级对象使用的,它既可以本当作值返回,还可以当作参数传递。理解了:“Js中的函数运行在它们被定义的作用域,而不是它们被执行的作用域”(摘自《JavaScript语言精粹》) 这句话即可。

题目:闭包优缺点

闭包封住了变量作用域,有效地防止了全局污染;但同时,它也存在内存泄漏的风险:

在浏览器端可以通过强制刷新解决,对用户体验影响不大
在服务端,由于node的内存限制和累积效应,可能会造成进程退出甚至服务器沓机
解决方法是显式对外暴露一个接口,专门用以清理变量:


function mockData() {
  const mem = {}
  
  return {
    clear: () => mem = null, // 显式暴露清理接口

    get: (page) => {
      if(page in mem) {
        return mem[page]
      }
      mem[page] = Math.random()
    }
  }
}
目录
1. ES5继承
2. 原型和原型链
2.1. 原型
2.2. 原型链
3. 作用域和作用域链
4. Event Loop
4.1. 单线程
4.2. 执行栈和任务队列
5. 执行上下文
6. 闭包的理解和分析

原文: https://godbmw.com/passages/2019-03-27-javascript-second/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值