JavaScriptNotes

JavaScript 简介

历史

  • Brendan Eich 10 days.

  • 1995年诞生,为处理输入验证操作。(LiveScript–>JavaScript)

  • 1997年,JavaScript 1.1 提交至欧洲计算机制造商协会(ECMA),并定义了 ECMAScript 新脚本语言标准。

  • ……

定义

JavaScript 是一种轻量级脚本语言

JavaScript 是一种嵌入式(embedded)语言。本身提供的核心语法不算很多,只能用来做一些数学和逻辑运算。本身不提供任何与 I/O(输入/输出)相关的 API,都要靠宿主环境(host)提供,所以 JavaScript 只合适嵌入更大型的应用程序环境,去调用宿主环境提供的底层 API。

目前,已经嵌入 JavaScript 的宿主环境有多种,最常见的环境就是浏览器,另外还有服务器环境,也就是 Node 、Deno等等。

组成

  • 核心 (ECMAScript)

    • 已经不断更新多次,到现在常用 ES5/ES6,直到如今更高版本。

      1997年7月ECMAScript 1.0
      1998年6月ECMAScript 2.0
      1999年12月ECMAScript 3.0
      2007年10月ECMAScript 4.0
      2009年12月ECMAScript 5.0
      2011年6月ECMAscript 5.1
      2015年6月ECMAScript 6(2015)
  • 文档对象模型 (DOM)

    • Document Object Model——文档对象模型)是用来呈现以及与任意 HTML 或 XML文档交互的API。DOM 是载入到浏览器中的文档模型,以节点树的形式来表现文档,每个节点代表文档的构成部分(例如:页面元素、字符串或注释等等);我们可以通过接口可以轻松的对节点进行添加、删除等操作。
  • 浏览器对象模型 (BOM)

    • Browser Object Model: 提供与浏览器进行交互的方法和接口。其标准并没有确切的,会因不同浏览器而异。

HTML 中应用 JavaScript

在HTML中以 <script></script> 标签内嵌,也可通过src来引入外部的一个.js文件.

Hello World

function hello(){
alert("hello Tadm~");
console.log("hello");
}

放置位置

在这之前我们需要了解HTML页面加载渲染的过程:

  1. 浏览器通过HTTP协议请求服务器,获取下载HMTL文档并开始从上到下解析,构建 DOM;
  2. 在构建 DOM 的过程中,如果遇到外联的样式声明和脚本声明,则暂停文档解析,下载样式文件和脚本文件;
  3. 样式文件下载完成后,构建 CSSDOM;脚本文件下载完成后,解释并执行,然后继续解析文档构建 DOM
  4. 完成文档解析后,将 DOM 和 CSSDOM进行关联和映射,最后将视图渲染到浏览器窗口。
  • 置于<head></head>标签中
    • 在加载页面前,需要先将所有的JS代码下载、解析、执行完,才会去编译页面内容。但会导致页面出现空白,因为代码解析执行是会有相应的时间,相当于处于等待状态。
  • 置于<body></body>标签中
    • 在页面代码前同样会出现在head标签中的问题;在页面代码后完美解决这个问题,当然也可以在script标签中添加asyncdefer关键字来指定其是异步加载的,可以先加载页面再去处理JS代码,但同时有async标记的则不会保证其执行顺序。

defer && async

Explanation

defer:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。只适用于外联脚本,保证了彼此的先后顺序。
async:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。不能确保彼此的先后顺序,会在load事件之前执行。

img

Useage

  1. 将脚本引用放置在DOM下方,紧接在您的</body>元素上方。
  2. 除非您要创建供他人使用的库,否则不要通过侦听 DOMContentLoadedload 事件来使代码复杂化。相反,请参阅上一点。
  3. 使用 defer属性标记引用外部文件的脚本。
  4. 如果您的代码不依赖于加载DOM并作为对文档中其他脚本的准备工作的一部分来运行,则可以将此脚本放置在页面顶部,并 在其上设置async属性。

noscript

有时候我们需要提醒用户的浏览器暂不支持script,当然现在主流浏览器基本上都已经支持并丰富了很多操作和功能,但是我们需要知道知识点。

<noscript>
<p>您的浏览器暂不支持 JavaScript</p>
</noscript>

基本语法

涉及到的其他程序语言共同处不再陈述。

数据类型

  • Number
  • Boolean (true/false)
  • String
  • undefined (未定义)
  • null (空)
  • Object
    • Object
    • Array
    • Function
  • Symbol (ES6)

变量

var a;
a = 1;
var a = 1;

let a;
a = 1;
let a = 1;

变量名,即标识符。大小写敏感,不能数字开头即可

var存在变量提升(所有的变量的声明语句,都会被提升到代码的头部)(JavaScript 引擎的工作方式是:先解析代码,获取所有被声明的变量,然后再一行一行地运行);而let则不会,只是在其作用域起效;同时也不能在同一作用域重复声明。

function f() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}

// IIFE 写法
(function () {
var tmp = ...;
...
}());

// 块级作用域写法
{
let tmp = ...;
...
}

常量

const name = 'tadm';

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错

// 冻结对象,使得不能再添加新属性
const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
// "use strict";
foo.prop = 123;

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

Number

JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此.

上述换句话说:JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数);这样对于浮点数的计算是非常危险的,因为计算并不是非常准确。

  • 64 位浮点数

    • 第 1 位:符号位,0表示正数,1表示负数
    • 第 2 位到第 12 位(共 11 位):指数部分
    • 第 13 位到第64位(共 52 位):小数部分(即有效数字)

    符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。

    指数部分一共有11个二进制位,因此大小范围就是0-2047。IEEE 754 规定,如果指数部分的值在0-2047之间(不含两个端点),那么有效数字的第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字这时总是1.xx...xx的形式,其中xx..xx的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript 提供的有效数字最长为53个二进制位。

    (-1)^符号位 * 1.xx…xx * 2^指数部分

绝对值小于2的53次方的整数,即-2^532^53,JS都可以精确表示

表示范围

-Infinity-002^-1023(开区间) ~ 2^1024Infinity

进制

  • 十进制:没有前导0的数值。
  • 八进制:有前缀0o0O的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。
    • 严格模式(”use strict”)无效,会使 JS 引擎抛出错误
  • 十六进制:有前缀0x0X的数值。
  • 二进制:有前缀0b0B的数值。

parseInt():

  • 将字符串转为整数

    parseInt('hello1234ddd')
    NaN
    parseInt('1234ddd')
    1234
  • 如果不是字符串时,则会先将其转换为字符串,再转为整数

    parseInt(123)	//	parseInt(String(123))
    123
  • 也可进行进制转换(返回十进制):parseInt('10',2) --> 2,如果输入了不包含当前进制的则会返回NaN

parseFloat():

  • 将一个字符串转为浮点数
  • 若字符串符合科学计数法,则会进行相应的转换

特殊值

  • +0(0)

  • -0

  • +Infinity (Infinity)(x/+0,Infinity/0,Infinity + Infinity ,Infinity * Infinity )

  • -Infinity (x/-0)

  • NaN (Not a Number)

    • 字符串解析成数字出错
    • 某些不符合正常的函数运算结果(0/0)
    • 0 * Infinity,Infinity - Infinity ,Infinity 、 Infinity
    • NaN === NaN —> false
    • [NaN].indexOf(NaN) —> -1 (indexOf 严格相等)

    InfinityNaN比较,总是返回false

    isNaN(): 判断一个值是否为NaN

    • NaN –> true
    • !Number –> isNaN(Number(x))
    • 空数组和只有一个数值成员的数组 –> false

    isFinite(): 返回一个布尔值,表示某个值是否为正常的数值。

    ​ 只有以下才为false:

    • +-Infinity
    • NaN
    • undefined

位运算符

String

零个或多个排在一起的字符,放在单引号或双引号之中;默认写在一行,如果需要多行,则需要行末添加反斜杠\(此时输出仍在一行内)

length 属性返回字符串长度,且不能被更改

toString():

  • 将非字符串转换为字符串(为数值时可传入进制数)

反斜杠

  • 转义

  • \HHH

    反斜杠后面紧跟三个八进制数000377),代表一个字符。HHH对应该字符的 Unicode 码点,比如\251表示版权符号。显然,这种方法只能输出256种字符。

  • \xHH

    \x后面紧跟两个十六进制数00FF),代表一个字符。HH对应该字符的 Unicode 码点,比如\xA9表示版权符号。这种方法也只能输出256种字符。

  • \uXXXX

    \u后面紧跟四个十六进制数0000FFFF),代表一个字符。XXXX对应该字符的 Unicode 码点,比如\u00A9表示版权符号。

字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始);但不能用于增删改操作。

字符集

JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。当然还允许直接在程序中使用 Unicode 码点表示字符,即将字符写成\uxxxx的形式,其中xxxx代表该字符的 Unicode 码点。

解析代码的时候,JavaScript 会自动识别一个字符是字面形式表示,还是 Unicode 形式表示。输出给用户的时候,所有字符都会转成字面形式。

​ 字符内部存储:

- 每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存

​ 但是由于历史原因,初始设计时并未考虑到现在使用了如此多的字符!所以对于码点在U+10000U+10FFFF之间的字符,JavaScript 总是认为它们是两个字符(length属性为2)。所以处理的时候,必须把这一点考虑在内;也就是说,JavaScript 返回的字符串长度可能是不正确的。

Null && Undefined

  • null 表示空,即空对象指针

  • undefined 表示未定义,一般是声明了变量但并未对其初始化,该变量则为undefined;派生自null

    typeof null		//	"object"

    null == undefined // true
    null === undefined // false

Array

基本知识

一组值(任何类型)按次序排列(!排序),同C,JAVA等中的数组一样

​ 注意:本质上也是一种特殊的对象(typeof [1,2] –> “object”)

​ arr[1] —> arr[‘1’] —> 2

length属性是可写的:

  • 数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。
  • 人为设置一个小于当前成员个数的值时,该数组的成员会自动减少到length设置的值(设置为0时则相当于清空数组)。
  • 人为设置一个大于当前成员个数的值时,读取新增的位置数据时为undefined

如果数组的键名是添加超出范围的数值,该键名会自动转为字符串;同时通过方括号([-1])仍可以获得其值,但此时length还是为0

in:

  • 返回Boolean,判断某个元素是否存在于数组中

for…in…:

  • 遍历数组(但是会遍历他的所有键

    var a = [1, 2, 3];
    a.foo = true;

    for (var key in a) {
    console.log(key);
    }
    // 0
    // 1
    // 2
    // foo

    以后通常使用forEachmapfor...of...reducereduceRight……

类型检测

判断一个变量是否为数组:

  • x instanceof Array
  • Array.isArray(x)

栈方法(LIFO)

  • push(x, …) — 入栈并返回修改后数组的 length
  • pop() — (最后一项)出栈并返回被移除的项

队列方法(FIFO)

  • unshift(x, …) — 入队并返回 length
  • shift() — (第一项)出队并返回被移除的项

重排序

  • x.sort()
  • x.reverse()

操作方法

  • x.concat(y,…)
  • x.slice(start, [end]) — 返回新数组且不会影响原数组
  • x.splice()
    • x.splice(start, delNumber) — 从 start 开始删除 delNumber 项
    • x.splice(start, 0, insert1,….) — 从 start 开始插入 多项
    • x.splice(start, delNumber, insert1,…) — 从 start 开始删除 delNumber 项并插入多项,类似于替换效果

查找项的位置

  • x.indexOf(item, [start]) — 从前往后
  • x.lastIndexOf(item, [start]) — 从后往前

从数组中查找第一个符合条件的,若找到返回其索引,否则返回 -1 且在查找时使用 ‘===’ 进行判断。

Object

基本知识

image-20200322201420824

let obj = {
name: 'tadm',
age: 19,
say: function(){
console.log('I\'m saying')
}
}

obj.name // tadm
obj.age // 19
obj['age'] // 19
obj.say() // I'm saying

// 对象引用
var o1 = {};
var o2 = o1;

o1.a = 1;
o2.a // 1

var o1 = {};
var o2 = o1;

// 取消引用,此时 o1,o2 不再指向同一个内存地址
o1 = 1;
o2 // {}

属性

Object.keys(obj)	//	查看对象的所有自身可枚举属性

delete obj.name // 删除对象本身的属性,不包括继承(原型链)的属性

'toString' in obj // true

for ... in ... // 遍历对象所有可遍历的属性(自身+继承),但会跳过不可遍历的属性
for(let s in obj){
console.log(s)
}
// 如果我们只需要遍历自身的属性,不遍历继承的属性
for (let key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}

定义属性

Object.defineProperty

let obj = {
name: 'Tadm',
age: 20
}

// object default value
Object.defineProperty(obj, age, {
// 能否通过 delete 删除属性来重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性
configurable: true,
// 能否枚举
Enumerable: true,
// 能否修改属性的值
Writable: true,
// 包含这个属性的数据值,读取写入的值保存在此处
Value: undefined,
// 读取属性时调用的函数
get: undefined,
// 写入属性时调用的函数
set: undefined
})
// 在单独使用 Object.defineProperty() 方法时默认值为 false

以上为 false 时,我们仍要操作时在非严格模式下会将其忽略,严格模式下抛出错误。而且,一旦(configurable)属性定义为不可配置,就不能再修改为可配置。

var pattern = {
get: function () {
return 'I alway return this string,whatever you have assigned';
},
set: function () {
this.myname = 'this is my name string';
}
};

function TestDefineSetAndGet() {
Object.defineProperty(this, 'myproperty', pattern);
}

var instance = new TestDefineSetAndGet();
instance.myproperty = 'test';

// 'I alway return this string,whatever you have assigned'
console.log(instance.myproperty);
// 'this is my name string'
console.log(instance.myname);继承属性

get(only) — readOnly set(only) — writeOnly

Object.defineProperties

let obj = {}

Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
// etc. etc.
});

读取属性

Object.getOwnPropertyDescriptor

返回指定对象上一个自有属性对应的属性描述符(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)。如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined 。

var o, d;

o = { get foo() { return 17; } };
d = Object.getOwnPropertyDescriptor(o, "foo");
// d {
// configurable: true,
// enumerable: true,
// get: /*the getter function*/,
// set: undefined
// }

o = { bar: 42 };
d = Object.getOwnPropertyDescriptor(o, "bar");
// d {
// configurable: true,
// enumerable: true,
// value: 42,
// writable: true
// }

o = {};
Object.defineProperty(o, "baz", {
value: 8675309,
writable: false,
enumerable: false
});
d = Object.getOwnPropertyDescriptor(o, "baz");
// d {
// value: 8675309,
// writable: false,
// enumerable: false,
// configurable: false
// }

Object.getOwnPropertyDescriptors

获取一个对象的所有自身属性的描述符。

原型模式中例子理解。

Create Object

工厂模式

快速创建多个相似对象

function createPerson(name, age){
let obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
console.log(this.name);
};
return obj;
}

let person1 = createPerson('Tadm', 19);
person1.sayName();
let person2 = createPerson('Tadm1', 20);

// 缺点:无法确定对象的类型

构造函数模式

function Person(name, age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name);
};
}

let person1 = new Person('Tadm', 19);
person1.sayName();
let person2 = new Person('Tadm1', 20);

person1.constructor === Person; // true

// 也可以通过下列方式
Person('Tadm', 19);
window.sayName();

let o = new Object();
Person.call(o, 'Tadm', 19);
o.sayName();

// 缺点: 一样的我们却创建了多个实例
person1.sayName === person2.sayName // false

原型模式

function Person(){};

Person.prototype.name = 'tadm';
Person.prototype.age = 20;
Person.prototype.sayName = function(){
console.log(this.name);
};

let person1 = new Person();
person1.sayName();
let person2 = new Person();

person1.sayName === person2.sayName; // true

// isPrototypeOf(obj) 方法允许你检查一个对象是否存在于另一个对象的原型链上。
Person.prototype.isPrototypeOf(person1); // true

// Object.getPrototypeOf(obj) 方法返回指定对象的原型(内部[[Prototype]]属性的值)。
Object.getPrototypeOf(person1) === Person.prototype; // true

// obj.hasOwnProperty(property) 方法会返回一个布尔值,指示对象自身(实例非原型)属性中是否具有指定的属性(也就是,是否有指定的键)。
person1.hasOwnProperty('name'); // false
person1.name = 'tadm1';
person1.hasOwnProperty('name'); // true

Object.getOwnPropertyDescriptor(person1,'name')
// {value: "tadm1", writable: true, enumerable: true, configurable: true}
// __proto__: Object

person1.age = 19;
Object.getOwnPropertyDescriptors(person1);
// {name: {…}, age: {…}}
// name: {value: "tadm1", writable: true, enumerable: true, configurable: true}
// __proto__: Object
// age: {value: 19, writable: true, enumerable: true, configurable: true}
// __proto__: Object

// 缺点:原型中的所有属性是被实例共享的,基本类型无碍但是属性为引用类型(比如数组)时,一个实例修改某个属性时则会导致原型上的改属性也被修改,但可能往往另一个实例的该属性并不想要此变化。

组合使用构造函数模式和原型模式

非常常用,推荐!

function Person(name, age){
this.name = name;
this.age = age;
this.colors = ['red','green','blue']
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name)
}
}

let person1 = new Person('Tadm', 19);
person1.name = 'liu';
person1.sayName(); // liu
let person2 = new Person('Tadm1', 20);
person2.sayName(); // Tadm1

person1.age === person2.age; // false
person1.sayName === person2.sayName; // true

// 注意,当我们在用对象字面量重写 prototype 时会断掉实例和原型的联系,此时实例仍是引用之前的原型
Person.prototype = {
constructor: Person,
sayName: function(){
console.log('override')
}
}

person1.sayName() // liu
Person.prototype.sayName() // override

动态原型模式

function Person(name, age){
this.name = name;
this.age = age;
Person.prototype.sayName = function(){
console.log(this.name)
}
}

该模式时不能使用字面量形式重写原型,同样会切断实例和原型的联系。

寄生构造函数模式

不推荐,了解即可

稳妥构造函数模式

function Person(name, age){
let o = new Object();
o.sayName() = function(){
console.log(name)
}
}

let person1 = Person('Tadm', 19);
person1.sayName(); // Tadm

此时没有this 和 new 操作,只能通过 sayName() 来访问 name,为对象的数据成员提供了一个’稳妥’的环境。

而且同寄生构造函数模式一样,创建的对象与构造函数或者其原型没有关系。

Extends

组合继承(常用)

function SuperType(name){
this.name = name
this.colors = ['red','green','blue']
}
SuperType.prototype.sayName = function(){
console.log(this.name)
}

function SubType(name, age){
SuperType.call(this, name) // 第二次
this.age = age
}
SubType.prototype = new SuperType() // 第一次
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function(){
console.log(this.age)
}

let instance1 = new SubType('tadm', 19);
instance1.colors.push('auqa');
console.log(instance1.name, instance1.age, instance1.colors);
// tadm 19 (4) ["red", "green", "blue", "auqa"]

let instance2 = new SubType('tadm1', 20);
console.log(instance2.name, instance2.age, instance2.colors);
// tadm1 20 (3) ["red", "green", "blue"]

缺点:无论什么情况下都会调用两次超类型的构造函数(一次是在创建子类原型时,第二次是子类型构造函数内),这样就会产生不必要的、多余的属性;而且子类型会包含超类型所有的属性。

原型式继承

function object(o){
// 创建一个临时性构造函数
function F(){}
// 将传入对象作为此构造函数原型
F.prototype = o
// 返回此临时类型的一个实例
return new F()
}
let person = {
name: 'tadm',
age: 19
}
let anotherPerson1 = object(person)
anotherPerson1.name = 'tadm1'

let anotherPerson2 = Object.create(person, {
name:{
value: 'tadm2'
},
age: {
value: 20
}
})

同原型模式一样,引用类型值的属性会被实例所共享。

寄生式继承

function object(o){
function F(){}
F.prototype = o
return new F()
}
function createAnother(origin){
let clone = object(origin)
clone.sayHi = function(){
console.log('hi')
}
return clone
}

let person = {
name: 'tadm',
age: 19
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() // hi

寄生组合式继承(最理想)

构造函数继承属性,原型链的混成形式继承方法。

function object(o){
function F(){}
F.prototype = o
return new F()
}
function inheritPrototype(subType, superType){
// 创建超类型原型的副本
let prototype = object(superType.prototype)
// 为副本添加 constructor 属性,弥补重写原型而失去默认的该属性
prototype.constructor = subType
// 将副本赋值给子类型的原型
subType.prototype = prototype
}

function SuperType(name){
this.name = name
this.colors = ['red','green','blue']
}
SuperType.prototype.sayName = function(){
console.log(this.name)
}

function SubType(name, age){
SuperType.call(this, name)
this.age = age
}
inheritPrototype(SubType, SuperType) // 一次
SubType.prototype.sayAge = function(){
console.log(this.age)
}

类似数组的对象

// arguments对象
function args() { return arguments }
var arrayLike = args('a', 'b');

arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false

// DOM元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false

// 字符串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false

​ 如果我们想要将其变为真正的数组:

var arr = Array.prototype.slice.call(arrayLike);

function print(value, index) {
console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);

More

ObjectFunction

Function

Basic

JavaScript 语言中称函数为第一等公民(函数与其他数据类型地位平等)

//	声明定义
function print(s) {
console.log(s);
}

// 函数表达式
var print = function(s) {
console.log(s);
}; // 此处需要分号

// 通过Function构造函数(不推荐)
var add = new Function(
'x',
'y',
'return x + y'
);

// 等同于
function add(x, y) {
return x + y;
}

函数重复声明定义时会进行重载覆盖操作

声明定义方式与函数表达式方式区别:

  • 声明定义可在声明前调用。因为此时函数名类似于变量名(变量提升),整个函数则会被提升到代码头部。然而函数表达式(赋值)方式则不能在声明前调用,会抛出错误。
  • 但是采用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。
var myFunc = function () {};

function test(f) {
console.log(f.name); // 获取参数函数的名字
}

test(myFunc) // myFunc
test.length // 返回函数预期传入的参数个数,即函数定义之中的参数个数

函数作用域

  • 全局作用域

  • 函数作用域(外部不能访问读取修改)

    • 与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

    • 优先读取所在作用域变量,若没有才会沿上一层查找。(作用域链)

      function foo(x) {
      if (x > 100) {
      var tmp = x - 100;
      }
      }

      // 等同于
      function foo(x) {
      var tmp;
      if (x > 100) {
      tmp = x - 100;
      };
      }
  • 块级作用域(ES6)

闭包 (重点)

因为JS存在”链式作用域”,所以子对象会层级的向上寻找所需变量,同时父对象所有的变量对其子对象是可见的(很好的理解全局作用域)。

能够读取其他函数内部变量函数(定义在一个函数内部的函数)

用途:

  • 可以读取函数内部的变量

  • 让这些变量始终保持在内存中(JS 有内存回收机制),使得它诞生环境一直存在

    function createIncrementor(start) {
    return function () {
    return start++;
    };
    }

    var inc = createIncrementor(5);

    inc() // 5
    inc() // 6
    inc() // 7

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

立即调用的函数表达式(IIFE)

自调用匿名函数

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

用途:

  • 避免污染全局变量
  • IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量

eval (不常用,了解)

接受一个字符串作为参数,并将这个字符串当作语句执行

eval('var a = 1;');
a // 1

eval(123) // 123,非字符串时,原样返回

// eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值
// 为了防止这种风险,JavaScript 规定,使用严格模式时,eval内部声明的变量,不会影响到外部作用域
(function f() {
'use strict';
eval('var foo = 123');
console.log(foo); // ReferenceError: foo is not defined
})()

typeof

typeof 1	// "number"
typeof true // "boolean"
typeof 'tadm' // "boolean"
typeof undefined // "undefined"
typeof 未声明或者未初始化的变量 // "undefined"
typeof null // "object" (空对象引用)
typeof window // "object"
typeof {} // "object"
typeof [] // "object"

function f(){
console.log("my")
}
typeof f // "function"

instanceof

返回一个布尔值,表示对象是否为某个构造函数的实例(检查整个原型链);只能用于对象,不适用(七种)原始类型的值.

var v = new Vehicle();
v instanceof Vehicle // true
// Vehicle.prototype.isPrototypeOf(v)

null instanceof Object
// false
undefined instanceof Object
// false

var x = [1, 2, 3];
var y = {};
x instanceof Array // true
x instanceof Object // true
y instanceof Object // true
y instanceof Array // false
// 这样可以区分数组和对象的类型

var s = 'hello';
s instanceof String // false

基本结构体

if (a === 0) {
// ...
} else if (a === 1) {
// ...
} else {
// ...
}

var m = 1;
var n = 2;

if (m !== 1)
if (n === 2) console.log('hello');
else console.log('world');
// 打印 hello 因为 else 始终和距离最近的相匹配

let fruit = 'apple'
switch (fruit) {
case "banana":
// ...
break;
case "apple":
// ...
break;
default:
// ...
}

(condition) ? 表达式1 : 表达式2
// true to 1 false to 2

for(let i = 0; i <= 6; i++){
// ...
}

while(condition){
// ...
}
do{
// ...
} while(condition)

// break语句用于跳出代码块或循环
// continue语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环

top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) break top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0

RegExp

变量、作用域、内存

复制变量值

基本类型

  • 会在变量对象上创建一个新值,再将其复制到为新变量分配的位置上。因此我们操作新变量时不会影响原来的变量。

引用类型

  • 同基本类型原理一样,不过此值是一个指向变量值存储在堆(基本类型放于栈内存,Object等放于堆内存)中的地址的指针。此时他们两个就指向同一个内存地址,因此会互相影响。

传递参数

按值传递(将函数外的值传入函数内同变量(基本类型与引用类型)间的复制一样)

一般我们可以在函数内通过 arguments 对象来访问传递到函数的参数数组(arguments[0,1,…]),通过arguments.length 来获取参数个数。

类型检测

  • typeof
  • instanceof

执行环境及作用域

执行环境定义了变量或者函数有权访问的其他数据,决定了他们各自的行为。

代码在一个环境中执行时,会创建变量对象的一个作用域链。其用途是保证对执行环境有权访问的变量和函数的有序访问。

image-20200410092522058

没有块级作用域

if(true){
var color = 'aqua'
}
console.log(color) // aqua

for(var i=0; i<5; i++){
doSth(i)
}
console.log(i) // i=10

使用 var 声明的变量会自动被添加到最接近的环境中。

  • 函数内部 —- 函数的局部环境
  • with 语句 — 函数环境
  • 未使用 var 声明 — 该变量自动添加到全局环境

垃圾回收

  • 标记清除

    • 进入环境 — 不回收
    • 离开环境 — 回收
  • 引用计数(被引用的次数)

  • window.CollectGarBage()

解除引用

let a = 2
doSth()
a = null // 解除引用

解除引用并不是自动回收该值所占用的内存,而是让该值脱离执行环境,以便垃圾回收器下次运行时将其回收。

引用类型

后续持续更新,敬请期待!

参考资料

JavaScript
ES6

Author: 𝓣𝓪𝓭𝓶
Link: https://liuhongwei3.github.io/2020/03/21/JavaScriptNotes/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.