tomoya06 / web-developer-guidance

Actually it's just a notebook for keeping down some working experience.
4 stars 0 forks source link

JavaScript - Basic #10

Open tomoya06 opened 4 years ago

tomoya06 commented 4 years ago

本文参考前端进阶之道

语法基础

数据类型

参考MDN文档

typeof

// 基本类型
typeof 1 // 'number'
typeof NaN // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof 123n // 'bigint'
typeof b // b 没有声明,但是还会显示 undefined

// 对象、函数
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

// null
typeof null // 'object'

// 获得变量的正确类型
Object.prototype.toString.call(myClass) // "[object Type]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(1) // "[object Number]"

// 判断数组
Object.prototype.toString.call(obj) === '[object Array]'
obj instanceof Array === true
Array.isArray(obj) === true

类型转换

转Boolean

new Boolean(value)
// value = undefined, null, false, NaN, '', 0, -0 时为false,其他均为true
对象转基本类型
// 调用顺序:Symbol.toPrimitive【优先级最高】 -> valueOf -> toString
let a = {
    valueOf() {
        return 0
    }
}
1 + a // => 0
'1' + a // => '10'

// 比较

let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
  [Symbol.toPrimitive]() {
    return 2;
  }
}
1 + a // => 3
'1' + a // => '12'

转自MDN:对Symbol.toPrimitive的更多用法

// An object without Symbol.toPrimitive property.
var obj1 = {};
console.log(+obj1);     // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ''); // "[object Object]"

// An object with Symbol.toPrimitive property.
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == 'number') {
      return 10;
    }
    if (hint == 'string') {
      return 'hello';
    }
    return true;
  }
};
console.log(+obj2);     // 10        -- hint is "number"
console.log(`${obj2}`); // "hello"   -- hint is "string"
console.log(obj2 + ''); // "true"    -- hint is "default"

四则运算

  1. 注意加法运算:其中一方是字符串类型,就会把另一个也转为字符串类型。
  2. 其他运算只要其中一方是数字,那么另一方就转为数字。
1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'

// 解析:
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'

one more thing...

'a' + + 'b' // -> "aNaN"
// 因为 +'b' -> NaN
// 你也许在一些代码中看到过:+'1' -> 1
['10', '10', '10', '10', '10', ].map(parseInt) = [10, NaN, 2, 3, 4]

// DEBUG:
arr.map(Number)
arr.map(item=>parseInt(item, 10))

map的语法以及parseInt的语法

双等号比较

image

分析:[] == ![] // => true

![] = false
// 原等式转为:
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
// 原等式转为:
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// 原等式转为:
0 == 0 // -> true
tomoya06 commented 4 years ago

面向对象

概述

面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是一种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。

面向对象的三大特性

  1. 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  2. 继承:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
  3. 多态:允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。实现多态的方法有重写(Overriding)和重载(Overloading)。

重写 vs 重载

Java代码示例参考菜鸟教程

原型

image

下面总结来自这里

function Foo() {}
foo = new Foo()
foo.__proto__ === Foo.prototype // => true

new

过程

  1. 新生成一个空对象
  2. 链接到原型:把新对象的__proto__链接到构造函数的prototype
  3. 绑定this到这个空对象并执行构造函数
  4. 返回新对象

模拟实现:

function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数。别忘了arguments是类数组
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}

由此可见,如果在new函数中加了return:如果return值类型,那么对构造函数没有影响,实例化对象返回空对象;如果return引用类型(数组,函数,对象),那么实例化对象就会返回该引用类型;

instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

模拟实现:

function instanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype
    // 获得对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
        if (left === null)
            return false
        if (prototype === left)
            return true
        left = left.__proto__
    }
}

this

判断方法

  1. 当函数被当做构造函数使用,又new引导调用时,this只想new创建出来的对象。(new绑定);
  2. 函数通过apply,call,bind绑定时,this指向绑定的对象。(显式绑定)
  3. 当函数由一个对象引导调用时,this指向该对象。(隐式绑定)
  4. 当函数在没有任何修饰的情况下调用,非严格模式下,this指向window,严格模式下this指向undefined。(默认绑定)

优先级为:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定;

call、apply和bind

都是可以用来改变方法执行时this的指向。call和apply立即执行,但取参数的方法不同;bind只是改变指向并返回新的方法。

//在非严格模式下
var obj={ name: "new name" };
function fn(num1, num2){
    console.log(num1+num2);
    console.log(this);
}
fn.call(100,200);  //this->100 num1=200 num2=undefined
fn.call(obj,100,200);  //this->obj num1=100 num2=200
fn.call();  //this->window
fn.call(null);  //this->window
fn.call(undefined);  //this->window

//严格模式下 
fn.call();  //在严格模式下this->undefined
fn.call(null);  // 在严格模式 下this->null
fn.call(undefined);  //在严格模式下this->undefined

// call() vs apply() 传参方式不同
fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);

fn.call(obj,1,2);  //->改变this和执行fn函数是一起都完成了
fn.bind(obj,1,2);  
var tempFn = fn.bind(obj,1,2);  //->只是改变了fn中的this为obj并传递参数,但是此时并没有把fn这个函数执行
tempFn();   //这样才把fn这个函数执行

模拟实现:有点底层了,这里先忽略。参考这里

继承

听说有六种继承方式。优缺点参考这里

初始化:

//父类型
 function Person(name, age) {
   this.name = name,
   this.age = age,
   this.play = [1, 2, 3]
   this.setName = function () {}
 }
 Person.prototype.setAge = function () {}

 //子类型
 function Student(price) {
   this.price = price
   this.setScore = function () {}
 }

原型链继承

Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象

Student.prototype.sayHello = function () { }

借助构造函数

Person.prototype.setAge = function () {}
function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age)
    // 其他初始化...
}

Student.prototype.sayHello = function () { }

原型链+构造函数

下面几种关键差异在于如何延长原型链:

// begin
// 方法1:
Student.prototype = new Person()
Student.prototype.constructor = Student //组合继承也是需要修复构造函数指向的

// 方法2:
Student.prototype = Person.prototype

// 方法3:
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student

// end

Student.prototype.sayHello = function () { } 

ES6语法

class Person {
  //调用类的构造方法
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  //定义一般的方法
  showName () {
    console.log("调用父类的方法")
    console.log(this.name, this.age);
  }
}
let p1 = new Person('kobe', 39)
console.log(p1)

//定义一个子类
class Student extends Person {
  constructor(name, age, salary) {
    super(name, age)//通过super调用父类的构造方法
    this.salary = salary
  }
  showName () {//在子类自身定义方法
    console.log("调用子类的方法")
    console.log(this.name, this.age, this.salary);
  }
}
tomoya06 commented 4 years ago

面向函数

执行上下文

本节主要参考掘金翻译-理解 JavaScript 中的执行上下文和执行栈

定义:执行上下文(execution context)是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

每个执行上下文中都有三个重要的属性

当执行流进入一个函数时,执行上下文就会被推入一个上下文栈中,而在函数执行结束之后,栈将其上下文弹出,把控制权返回给之前的上下文。【来自红宝书】

作用域链

参考栈溢出网友的回答,每个函数都会创建一个自己的作用域,并且会连接父级作用域。

function foo(a, b) {
    var c;

    c = a + b;

    function bar(d) {
        alert("d * c = " + (d * c));
    }

    return bar;
}

var b = foo(1, 2);
b(3); // alerts "d * c = 9"

作用域链示意图:

+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
|   global binding object   |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| ....                      |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
             ^
             | chain
             |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| `foo` call binding object |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| a = 1                     |
| b = 2                     |
| c = 3                     |
| bar = (function)          |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
             ^
             | chain
             |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| `bar` call binding object |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| d = 3                     |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+

分类

注意js没有块级作用域,即如同类C语言中,用花括号封闭的代码块有自己的作用域,即js中所谓的执行上下文。

闭包

定义:函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

实际应用

  1. 解决for循环+异步执行问题。参考前端进阶之道的说明
  2. 用闭包模拟私有方法。参考MDN-闭包

生命周期

  1. 创建阶段:当函数被调用,但未执行任何其内部代码之前,会做以下三件事:
    1. 创建变量对象:首先初始化函数的参数arguments,提升函数声明和var变量声明。
    2. 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
    3. 确定this指向:包括多种情况,下文会详细说明
  2. 执行阶段:执行变量赋值、代码执行
  3. 回收阶段:执行上下文出栈等待虚拟机回收执行上下文

浅拷贝、深拷贝

深浅拷贝都是已经创建了一个新的对象,区别在于对深层级对象的拷贝效果

lodash参考浅拷贝_.clone(value)深拷贝_.cloneDeep(value)

详细实现指导可以参考这篇博客。自己实现代码参考这里

-- 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

箭头函数

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

arguments

类数组。有下面的特性:

image

IIFE

立即调用函数表达式。形式如下。这个匿名函数拥有独立的词法作用域。好处是不会污染全局作用域,且表达式中的变量不能从外部访问。

(function () {
    statements
})();
tomoya06 commented 4 years ago

ES6的新玩意儿

一览

Promise

Promise 是 ES6 新增的语法,解决了回调地狱的问题。

可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolve 和 reject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

promise手写实现参考这里,是抄的作业

Generator

Generator 是 ES6 中新增的语法,和 Promise 一样,都可以用来异步编程

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。

用得少,暂时先不手动实现。参考这里

async/await

一个函数如果加上 async ,那么该函数就会返回一个 Promise。可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。

async function test() {
  return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}

Proxy

参考MDN

const target = {
  message1: "hello",
  message2: "everyone"
};
const handler2 = {
  get: function(target, prop, receiver) {
    return "world";
  }
};
const proxy2 = new Proxy(target, handler2);
tomoya06 commented 3 years ago

严格模式

简介

定义:ECMAScript 5的严格模式是采用具有限制性JavaScript变体的一种方式,从而使代码显示地 脱离“马虎模式/稀松模式/懒散模式“(sloppy)模式。

特性:

  1. 严格模式通过抛出错误来消除了一些原有静默错误。
  2. 严格模式修复了一些导致 JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快。
  3. 严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法。

开启严格模式

  1. 为整个脚本开启:要在所有语句之前放一个特定语句use strict;
  2. 为函数开启:放在函数体所有语句之前

语法变化

完整语法变化参考MDN或者阮一峰的博客。下面只列几点自认为比较重要的。

tomoya06 commented 3 years ago

var let const

区别

参考StackOverflow

作用域在哪?

参考壹题回答

实现原理

参考壹题解答

变量生命周期

参考这篇issue,原文来自英文博客

  1. 声明阶段:是在作用域中注册一个变量
  2. 初始化阶段:是分配内存并为作用域中的变量创建绑定,在此步骤中,变量将使用undefined自动初始化
  3. 赋值阶段:是为初始化的变量赋值

具体实现:

tomoya06 commented 3 years ago

迭代器Iterator / 生成器Generator

可迭代 vs 迭代器

两个协议

可迭代协议

对象可迭代,必须实现@@iterator方法,也就是obj[Symbol.iterator] = someFunction

js内置的可迭代对象:String、Array、TypedArray、Map 和 Set 需要传入可迭代对象的语法: for...of 循环、展开语法、yield*,和结构赋值

迭代器协议

对象要成为迭代器,必须实现next()方法,next()方法要能返回一个对象,包含done[boolean], value两个属性

生成器

生成器函数使用function*语法编写。 最初调用时,生成器函数不执行任何代码,而是返回Generator迭代器。然后要调用generator.next()方法,执行到下一个yield为止。

async/await

async / await 是 Generator / yield 加上 Promise的语法糖。

有点存疑。参考阮一峰ES6入门壹题回答

tomoya06 commented 3 years ago

常用接口

对象相关

Object.defineProperty()

语法:Object.defineProperty(obj, prop, descriptor)。其中descriptor包括:

另外,object实例、class定义中也可以使用get/set关键字来定义getter/setter。object实例中使用与defineProperty的效果类似,属性都将被定义在实例自身上;而在class中使用时,属性将被定义在实例的原型上。参考MDN

for...in vs for...of

for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性,不应该用于数组。

for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环

for...in循环出的是key,for...of循环出的是value

// 遍历数组:
for(let index in aArray){
    console.log(`${aArray[index]}`); // 注意,除了数组元素外还会把aArray的自定义属性也打印出来
}
// 几乎等价于
for(var value of aArray){
    console.log(value);
}