Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript - Basic #10

Open
tomoya06 opened this issue Aug 29, 2020 · 7 comments
Open

JavaScript - Basic #10

tomoya06 opened this issue Aug 29, 2020 · 7 comments

Comments

@tomoya06
Copy link
Owner

tomoya06 commented Aug 29, 2020

本文参考前端进阶之道

语法基础

数据类型

参考MDN文档

  • 七种原始类型,可以用typeof直接确定类型:
    • undefined : typeof instance === "undefined"
    • Boolean : typeof instance === "boolean"
    • Number : typeof instance === "number"
    • String : typeof instance === "string"
    • BigInt : typeof instance === "bigint"
    • Symbol : typeof instance === "symbol"
    • null : typeof instance === "object". Special primitive type having additional usage for its value: if object is not inherited, then null is shown;
  • 引用类型:
    • Object : typeof instance === "object". Special non-data but Structural type for any constructed object instance also used as data structures: new Object, new Array, new Map, new Set, new WeakMap, new WeakSet, new Date and almost everything made with new keyword;
    • Function : a non-data structure, though it also answers for typeof operator: typeof instance === "function". This is merely a special shorthand for Functions, though every Function constructor is derived from Object constructor.

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
Copy link
Owner Author

tomoya06 commented Aug 29, 2020

面向对象

概述

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

面向对象的三大特性

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

重写 vs 重载

Java代码示例参考菜鸟教程

  • 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写。
  • 重载是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

原型

image

  • 每个函数都有 prototype 属性,该属性指向原型。特例 Function.prototype.bind() 也是函数,但没有prototype属性。
  • 每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 __proto__ 来访问。

下面总结来自这里

  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建
  • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
  • 函数的 prototype 是一个对象,也就是原型
  • 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链
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

  • JS中的this代表的是当前行为执行的主体
  • this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
  • 箭头函数的this看外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window

判断方法

  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
Copy link
Owner Author

tomoya06 commented Aug 29, 2020

面向函数

执行上下文

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

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

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

  • 变量对象(variable object):包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问;函数上下文只能访问到活动对象
  • 作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了):可以把它理解成包含自身变量对象和上级变量对象的列表
  • this

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

作用域链

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

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                     |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+

分类

  • 全局执行上下文:这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文: 每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • Eval 函数执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以这里忽略。

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

闭包

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

实际应用

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

生命周期

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

浅拷贝、深拷贝

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

  • 浅拷贝:对象第一层的值类型都能拷贝,引用类型无法拷贝只能传递指针。可使用Object.assign
  • 深拷贝:对象所有层级的值类型和引用类型都能拷贝。可使用JSON.parse(JSON.stringify(object)),但注意json不支持的变量类型会被忽略,如undefined/symbol/function,有循环引用的变量会报错;完整实现则需要使用递归拷贝。

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

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

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

箭头函数

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

arguments

类数组。有下面的特性:

image

  • 可迭代,即有arguments[Symbol.iterator]方法
  • arguments.callee()。严格模式禁用(好像也没用过

IIFE

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

(function () {
    statements
})();

@tomoya06
Copy link
Owner Author

tomoya06 commented Aug 29, 2020

ES6的新玩意儿

一览

  • let+const
  • 新类型:Symbol
  • Promise, Set, Map
  • class语法
  • Iterator, Generator
  • import/require

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
Copy link
Owner Author

tomoya06 commented Sep 12, 2020

严格模式

简介

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

特性:

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

开启严格模式

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

语法变化

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

  • 无法创建全局变量,即不用var / let / const而直接声明并赋值
  • 一些保留字,如eval / arguments不能被绑定或赋值;在未来版本会被使用的关键字如implements/interface/let/package等,不能作为变量名或形参名
  • 对象不能拥有重名属性

@tomoya06
Copy link
Owner Author

tomoya06 commented Sep 12, 2020

var let const

区别

参考StackOverflow

  • let / const在ES6引入
  • 作用域:var是函数作用域,只能被function(){}限制住;let是块级作用域,可以被{}限制住
  • 变量提升:var有变量提升,在声明前可以引用、可以赋值;let没有,声明前引用的话会导致ReferenceError,这就是所谓的暂时性锁区
  • 全局声明:在script根级声明var变量会被绑定到window;let不会,
  • 重定义:strict mode下,var可以重定义,let重定义会报错

作用域在哪?

参考壹题回答

  • 如果在block内声明let,就留在block内;如果在script最外层,就在script;
  • 在block内声明var,没有限制,例如for loop+timeout例子;在function内,就留在function内;在最外层,就挂在window

实现原理

参考壹题解答

变量生命周期

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

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

具体实现:

  • var:在任何语句执行前都已经完成了声明和初始化
  • let:完成声明,没有初始化;而且在栈内存分配变量时做一个检查,如果已经有相同变量名存在就会报错
  • const、class:同let。不过const存储的变量是不可修改的,且const语法限制声明一定要赋值,所以相当于声明+初始化+赋值都已经完成
  • function:已完成声明、初始化、赋值,所以函数的变量提升优先级更高

@tomoya06
Copy link
Owner Author

tomoya06 commented Sep 12, 2020

迭代器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
Copy link
Owner Author

tomoya06 commented Sep 13, 2020

常用接口

对象相关

Object.defineProperty()

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

  • configurable: boolean。true表示它的descriptor还可以被改变
  • enumerable: boolean。true表示可以出现在枚举属性中,例如被for...of轮询到
  • writable: boolean。true表示value可改
  • value: any
  • get: function
  • set: function

另外,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);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant