认识JavaScript细节(上)

[TOC]

0. 前戏

0.1 弹框

0.1.1 alert 原理

将 alert 传入的值运算,使用 toString 进行打印输出(类型为 string

0.1.2 confirm

确认请求框:原理与 alert 一致,有确定与取消两个框,可以接受选择的值

1
2
var flag = confirm("msg");
flag; //true 确定 false 取消

0.1.3 prompt

在 confirm 基础上添加输入框,返回输入值

0.2 console

0.2.1 console.log

控制台打印

0.2.2 console.dir

比 log 更详细

0.2.3 console.table

把 json 按照表格输出

1. 数据类型

1.1 基本数据类型

  • number

    • 普通数字

      parseInt/parseFloat:

      1. parseInt:把字符串中的 int 值提取出来

      2. parseFloat:把字符串中的 float 解析出来

        1
        2
        3
        parseInt('0.5s') // 0
        parseInt('0.5s') // 0.5
        parseInt('s0.5') //NaN

        从字符串最左边开始提取字符, 遇到第一个非有效字符略过

    • NaN(not a number)

      isNan([value])可以来检测当前的值是否不是有效机制

      isNaN 检测机制:

      1. 验证当前检测值是否为数字类型,如果不是浏览器会转换为数字类型
      2. 如果当前数字是数字类型,是有效数字返回 false,不是返回 true
      3. 其中数字类型转换规则:使用 Number([value])方法转换
        1. 字符串转换数字必须全部是有效字符,否则为 NaN,可识别小数
        2. boolean 转数字 true->1 flase->0
        3. 引用转换数字:toString 转换字符串后用字符串转换机制转换
        4. 其他:null->0 undefined->NaN [] -> ‘’ -> 0

      NaN 的比较:

      1. NaN 与谁都不相等(包括 NaN 自己)

        1
        Number(num) == NaN; //必定false,错误用法
  • string:

    • 字符串是存储在栈内存的
    • js 的字符串有不可变长的特性
  • boolean

    • 转换为 boolean
      1. Boolean:(转换为数字,0 和 NaN 会转换为 flase,其他为 true)
      2. !:先把其他类型转 boolean 取反
      3. !!:不取反,与 Boolean 基本没有区别
    • 规律:
      1. 只有 0 NaN ‘’ null undefined 五个值转换为 boolean 的 flase,其余为 true
  • null

    • 空对象指针:指预期之中不存在(人为),后期会操作
    • typeof null === ’object‘是 true 为什么 null 是个基本数据类型?
      • 因为计算机存储将 000 开头标识为对象,null 存储时候将所有位置 0,所以会出现 typeof null === true,但是不能说明 null 是引用数据类型
  • undefined

    • 未定义:非预期不存在(变量提升等情况),是浏览器自主情况

1.2 引用类型

  • 对象(Object)

    • 数组对象

    • 普通对象

      • 当我们存储属性名不是字符串或数字会转换为字符串(toString)存储
    • 正则对象

    • ……

  • 函数(function)

1.3 Symbol 类型(ES6)

创建一个唯一的值

1
2
3
var a = Symbol("test");
var b = Symbol("test");
console.log(a == b); //false

1.4 数据类型检测

四种方式

  1. typeof
  2. constructor
  3. Object.prototype.toString.call()
  4. instanceof

1.5 数据转换规则

  1. 转化为 number:

    1. 情况:

      1. isNaN 检测时
      2. 手动转换
      3. 运算符计算自动转换
      4. ==比较转换
    2. 规律:

      1. string->number:Number() ‘’->0 ‘ ‘ -> 0 ‘\n’ -> 0 ‘\t’ -> 0
      2. boolean->number:true->1 false->0
      3. 没有转换:null->0 undefined -> NaN
      4. 引用转换:toString 后转换
  2. 转换为 string:

    1. 情况:

      1. 基于+进行字符串拼接时候
      2. 吧引用转换为数字前
      3. 给对象设置属性名
      4. 手动调用 toString/toFixed/join/String 方法
    2. 规律:

      1. number->‘number’
      2. NaN -> ‘NaN’
      3. [1,2,3] -> 1,2,3 []->‘’
      4. 对象 -> ‘[object Object]’
  3. 特殊情况:

    在‘==’进行比较时候如果左右数据类型不相等则转换相同类型后比较

    1. 对象==对象:比较地址
    2. 对象==数字:对象转数字
    3. 对象==boolean:对象转数字,布尔转数字
    4. 对象==字符串:对象转数字,字符串转数字
    5. 字符串==数字:都转数字
    6. null == undefined:true

1.6 数组操作

  1. push(any):number:数组末尾追加新内容

  2. pop():any:删除最后一项

  3. shift():any:删除第一项

  4. unshift(any):number:向数组开头新增加内容

  5. splice(n:number[, m:number]):any:删除从 n 开始 m 个内容,返回删除内容

    不指定 m 则删除全部

    splice 可以使用 splice(2,0,….)实现增加,也可以添加删除个数修改

  6. slice(n:number, m:number):array:按照条件查找数组内容

  7. concat(array…):array:实现多个数组拼接,返回拼接后数组,原数不变

  8. join(any):指定连接符,对数组转换字符串拼接

  9. reverse():any:数组倒叙

  10. sort([function]):any:返回排序后数组

    1
    2
    3
    function(a, b){
    return a-b;//升序 return b-a; 降序
    }
  11. indexOf/lastIndexOf(any):number:返回当前项在数组中第一次(最后一次)出现索引

  12. 去重策略:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //1. 分别取出每一项与后面向每一项比较,删除重复项
    for (var i = 0; i < arr.length - 1; i++) {
    var item = arr[i];
    for (var k = i + 1; k < arr.length; k++) {
    if (item == arr[k]) {
    arr.splice(k, 1);
    k--; //此处解决数组塌陷问题
    }
    }
    }
    //2. 创建空对象,遍历数组每一项,吧存储值当做key与value存储,添加前看属性是否已经存在key
    //3. set去重

    1.7 字符串操作

所有单引号和双引号包起来的都是字符串

  1. charAt、charCodeAt:根据所以呢获取指定位置字符(unicode 编码)
  2. indexOf、lastIndexOf
  3. slice:获取索引区间字符串,支持负索引
  4. subString:获取范围字符串,不支持负索引
  5. substr:获取从 m 开始 n 个字符
  6. toUpperCase、toLowerCase
  7. split:按指定字符切割

1.8 Math 运算

  1. abs:绝对值
  2. ceil、floor:向上向下取整
  3. round:四舍五入
  4. sqrt:开方
  5. pow:取幂
  6. max、min:获取多个数最大最小值
  7. PI:圆周率
  8. random:获取 0-1 随机数(Math.random()*(m-n)+n)

1.9 函数

  1. arguments:一个类数组,不能使用数组方法,存储所有实参参数
    1. length:长度
    2. callee:函数本身

2. 语法细节

2.1 变量的提升

JS 作用域?

没有 let 时候,JS 中只有三种作用域:全局,函数,eval

js 作用域是静态的作用域(定义时候产生)

执行函数时,会产生执行上下文(EC),这个 EC 会压入 EC 栈(ECS)

函数的执行

分为创建阶段与执行阶段

其中不得不提到 VO 与 AO

VO 与 AO

可以理解 VO 为一个与执行上下文关联的对象,这个对象的属性是这个执行上下文中的声明的函数、变量、参数列表作为其属性。

AO 可以肯做是与 VO 相同的东西,在函数调用时,VO 被激活成了 AO。

未进入执行阶段之前,变量对象(VO)中的属性都不能访问!但是进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。

进入上下文阶段,AO 会做如下初始化:

  • 函数的所有形式参数
  • 所有函数声明,这个属性由一个函数对象的名称和值组成如果变量对象已经存在相同名称的属性,则完全替换这个属性
  • 所有变量声明,这个属性由变量名称和 undefined 值(系统默认初始值)组成;如果变量名称跟已经声明的形式参数或函数相同,则变量声明被忽略。

下面举个 AO 构建的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn(a, b){
var c = 10;
var d = function() {}
function e(){}
}

// Ao:
AO = {
a: 10,
b: undefined,
e: <ref to function total>.
d: undefined
}

这也就揭示了所谓的变量提升机制。

2.2 带 var 与不带 var 区别

带 var 是一种变量声明,会将变量映射到 window 上

不带 var 是一种对 window.A 变量的赋值动作,是一个为对象添加属性的行为

1
2
3
console.log(window.A); //undefined
console.log("A" in window); //true
var A;
1
2
3
4
5
console.log(window.A); //undefined
console.log("A" in window); //false
A = 1;
console.log(window.A); //1
console.log("A" in window); //true

2.3 变量重名处理机制

对于一个变量,无论是属于 function 还是普通变量,只要在同一个作用域,比如全局作用域声明就会被映射为 window 的一个属性,其中显然 window 是不会存在两个同名的属性,所以会触发变量的重新赋值机制:

1
2
3
4
5
6
fn(); //this is a function
function fn() {
console.log("this is a function");
}
var fn = 1;
fn(); //TypeError: fn is not a function

2.4 作用域链

带 var 的声明变量为当前作用域变量,与上级作用域无关

不带 var 变量不是私有变量,会向上级作用域查找,如果是上级作用域的变量则为这个作用域变量,不是上级作用域则继续向上查找,一直找到 window 为止

如果在 window 中仍然不存在这个属性则会为 window 添加这个属性

1
2
3
4
5
6
7
8
9
10
console.log(a, b); //undefined undefined
var a = 1,
b = 2;
function t() {
console.log(a, b); //undefined 2 变量在作用域链查找
var a = (b = 3);
console.log(a, b); //3 3
}
t();
console.log(a, b); //1 3

在 js 中存在以下几种作用域:

  1. 全局作用域
  2. 私有作用域(函数)
  3. 块级作用域(ES6):一般用大括号包含代码块,(对象的大括号不是)

2.5 ES6 的暂时性死区(TDZ)

ES6 规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。

同理,在 let 或 const 声明前使用 typeof 会造成错误

1
2
3
4
5
console.log(typeof a); //ReferenceError: Cannot access 'a' before initialization
const a = 1;

var x = x; //true
let x = x; //false

2.6 全局变量与私有变量

1
2
3
4
5
6
7
8
9
10
11
arr = [3, 2, 1];
function t(arr) {
console.log(arr); //[3,2,1]
arr[0] = 1;
arr = [100];
arr[0] = 2;
console.log(arr); // [2]
}

t(arr);
console.log(arr); //[1,2,1]

2.7 执行作用域

非严格模式存在以下两个属性

arguments.callee:当前函数本身

arguments.callee.caller:当前函数调用的宿主环境,全局环境为 null

在非严格模式下,arguments 中的变量与形参变量存在映射机制,如果修改 arguments 变量会修改实参的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var n = 1;
function fn() {
var n = 2;
function f() {
n++;
console.log(n);
}
f();
return f;
}
var x = fn(); //3
x(); //4
x(); //5
console.log(n); //1

2.8 闭包与应用

函数执行形成一个私有作用域,保护里面变量不受外接影响,这种机制叫做闭包。

作用:

  1. 保护私有变量不受外界变量影响
  2. 形成不销毁的栈内存,把它保存下来方便调取

格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
//惰性函数
(function () {
return {
//TODO
};
})();

//柯里化函数
function fn() {
function f() {
//TODO
}
}

应用:

  1. 异步编程中保存状态
1
2
3
4
5
6
7
8
9
10
11
12
13
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 0);
} // 3 3 3
//使用闭包
for (var i = 0; i < 3; i++) {
(function (i) {
return setTimeout(() => {
console.log(i);
}, 0);
})(i);
} // 0 1 2

上面的例子,每次循环添加一个不销毁的私有作用域,使得可以存储私有值,这种方式很浪费性能,实际使用中可以使用 let 解决这个问题。

  1. 封装业务开发模型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let productRender = (function () {
//AJAX获取数据
let productData = null;
let getData = function () {
//TODO
let xhr = new XMLHttpRequest();
xhr.open("GET", "json/product.json", false);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
productData = JSON.parse(xhr.responseText);
}
};
xhr.send(null);
};
//数据绑定
let bindHTML = function () {
//TODO
};
let bindEvent = function () {
//TODO
};
return {
init: function () {
getData();
bindHTML();
},
};
})();

productRender.init();

优点:

  1. 将一个变量长期保存内存中
  2. 避免全局变量污染;
  3. 私有成员的存在。

缺点:

  1. 常驻内存,增加内存使用量;
  2. 使用不当造成内存泄漏。

2.9 OOP

2.9.1 new

对于函数使用 new 命令,相当于使用构造函数方式进行对象的创建,在复杂类型的创建过程中,字面值创建和构造函数创建没有区别,但是在基本数据类型的创建中存在一定不同:

1
2
3
4
var num1 = 1;
var num2 = new Number(1);
typeof num1; //number
typeof num2; //object

虽然两者的类型不同,但是同样可以使用原型链上提供的属性和方法

下面我们来看 new 的原型机制:

普通函数的执行中,存在以下几个步骤:

  1. 形成私有作用域
  2. 形参赋值
  3. 变量提升
  4. 代码执行
  5. 栈内存释放

构造函数执行中,存在以下几个步骤:

  1. 形成私有作用域(栈内存)
  2. 形参赋值
  3. 变量提升
  4. 在当前私有栈创建一个对象,并让函数 this 指向这个对象
  5. 代码自上而下执行
  6. 代码执行完成后返回对象地址

细节:

当我们的构造函数不存在 return 时,浏览器会返回自动构造的实例,但是如果自己返回了一个基本值时,对构造实例没有影响,但是若是返回了一个对象,会覆盖默认实例,如果必须使用 return 则使用 return;

当构造函数不存在参数,可以省略小括号

下面简单实现一个 new:

1
2
3
4
5
6
7
8
9
10
function mockNew(constructorFunc, ...args) {
const prototype = constructorFunc.prototype;
const instance = Object.create(prototype);
const ret = constructorFunc.apply(instance, args);
if (ret instanceof Object) {
return ret;
} else {
return instance;
}
}

2.9.2 原型链设计模式

  • 原型(prototype)
  • 原型链(__proto__)

在 js 中,所有函数类型都自带一个属性:prototype,这个属性的值是一个对象,浏览器会为它开辟一个堆内存,而这个堆内存中存在一个属性:contructor,存储当前函数本身

每一个对象都有__proto__属性,这个属性指向当前所属类的 prototype,如果不能确定是谁的实例就是 Object 实例

1
Array.prototype.constructor === Array; //true

Selection_029

原型链查找机制:

当我们需要的属性在私有属性空间不存在时,会基于__proto找到所属类的 prototype,如果不存在则会基于_proto__继续查找,直到找到为止,否则会抛异常

2.10 this

2.10.1 指向总结

  1. 谁调用指向谁
  2. 没人调用指向 window
  3. 立即执行函数中 this 指向 window
  4. 括号表达式含有多项时 this 为 window,只有一项时候为调用对象

2.10.2 apply & call & bind

call 的执行原理:

  1. 把函数关键字中的 this 替换为 call 方法第一个传递的实参
  2. 把 call 方法第二个和以后的实参获得后,将要操作函数执行,并吧参数给予操作函数

es3 实现方法大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.es3Call = function (context) {
var content = context || window;
content.fn = this;
var args = [];
// arguments是类数组对象,遍历之前需要保存长度,过滤出第一个传参
for (var i = 1, len = arguments.length; i < len; i++) {
// 避免object之类传入
args.push("arguments[" + i + "]");
}
var result = eval("content.fn(" + args + ")");
delete content.fn;
return result;
};

上面的代码使用了 eval 执行的

1
2
3
4
5
6
7
8
9
10
function fn1() {
console.log(1);
}
function fn2() {
console.log(2);
}
fn1.call(fn2); //1
fn1.call.call(fn2); //2
Function.prototype.call(fn1); //
Function.prototype.call.call(fn1); //1

上面的例子可以看到,

首先第一个例子相当把 fn 中的 this 变成了 fn2,但是因为 fn1 没有使用 this,所以没有受到影响,直接运行 fn1。

第二个例子中,首先调用第二个 call,将第一个 call 函数的 this 变为 fn2,并执行了第一个 call 函数,相当于执行了 this()函数

第三个例子因为 call 只是向原型中的 call 传参,没有调用任何 call,所以不存在返回值

第四个例子与第二个相同,都是相当于调用了第一个 call 时候执行 this()

apply 的执行原理

apply 与 call 基本相同,只是第二个参数为数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.es3Apply = function (context, arr) {
var ctx = context || window;
ctx.fn = this;
var result;
if (!arr) {
result = ctx.fn();
} else {
var args = [];
for (var i = 0; i < arr.length; i++) {
args.push("arr[" + i + "]");
}
result = eval("context.fn(" + args + ")");
}
delete context.fn;
return result;
};

bind

bind 会返回一个新的函数,其中的 this 将会转换为第一个参数,之后的一序列参数会作为传递实参前的参数

他有以下几个特点:

  • bind 可以绑定 this 指向
  • bind 可以返回绑定指向后的函数
  • 如果绑定后的函数被 new 了,则当前函数的 this 就是当前的实例
  • new 的对象可以找到原函数的 ptototype
1
2
3
4
5
6
7
8
9
10
11
12
Function.prototype.es6Bind = function (context, ...rest) {
if (typeof this !== "function") throw new TypeError("invalid invoked!");
var self = this;
return function F(...args) {
if (this instanceof F) {
const obj = new self(...rest, ...args);
obj.setPrototypeOf(self.prototype);
return obj;
}
return self.apply(context, rest.concat(args));
};
};

2.10.3 使用 apply 来获取最大值

1
2
3
4
5
6
//正常方案
let arr = [1, 2, 3, 4, 1, 23];
eval("Math.max(" + arr.toString() + ")");
Math.max(...arr);
//apply作为参数的方案
Math.max.apply(null, arr);

2.11 优化

2.11.1 DOM 的回流(reflow)和重绘(repaint)

浏览器渲染页面有以下几个步骤:

  1. 计算 DOM 结构(DOM tree)【仅仅计算结构】
  2. css 加载
  3. 生成渲染树(render tree)【渲染样式】
  4. 浏览器基于 GPU 开始按照 render tree 对页面进行构建

重绘:就是当某一个元素的样式进行更改(位置没有更改)浏览器会按照最新样式重新绘制元素

优化方案:

​ 当修改多个样式时,可以使用 class 的切换来一次性更改,避免因为多次更改样式造成的重绘对页面性能的影响

//TODO 后期继续补充

2.12 正则整理

js 正则与其他语言基本相同,此处只整理元字符和常用 API

应用场景:

  1. 正则匹配
  2. 正则捕获

正则创建:

  1. let reg = /......./g:字面量方式
  2. let reg = new RegExp("^\\d+$", 'g'):构造函数方式

正则的元字符:

符号 表示
\d 数字
\D 除数字外所有字母
\w word 表示数字大小写和下划线
\W 非 word 非数字大小写和下划线
\s 空白字符[ \t\v\n\r\f]
\S 非空白字符
. 通配符

正则的量词:

符号 表示
? {0,1}
+ {1,}
+ {1,}
* {0,}

正则的修饰符:

修饰符 描述
i 执行对大小写不敏感匹配
g 全局
m 执行多行匹配

正则函数:

函数 描述
test(s:string):boolean 检测一个字符串是否匹配某个模式
exec(s:string):array 返回匹配第一个结果数组
str.seach(reg):number 返回匹配成功位置
str.replace(reg, s) 将匹配位置
str.match(reg):array 设置全局匹配所有结果到数组返回

关于[]()

  • []:

    1. 一般的元字符都代表本身含义
    2. 中括号中出现两位数,不是两位数,而是两个数字中的任意一个
  • ():括内

    1. 改变匹配优先级
    2. 分组捕获:使用 exec 捕获
    3. 分组引用:\1,\2….. 表示和第 1、n 个分组出现完全相同的内容(一括号一份组)

常用正则:

1
2
3
4
5
6
7
8
//有效数字
let reg = /^(+|-)?\d|[1-9]\d+\.\d+?$/;
//电话号
let reg = /^1\d{10}$/;
//中文姓名 汉字的unicode:[\u4E00-\u9FA5]
let reg = /^[\u4E00-\u9FA5]{2,}(·[\u4E00-\u9FA5]{2,})?$/;
//邮箱
let reg = /^\w+((-\w+)|(\.\w+))*@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/;

match 与 exec 之间的优缺点:

  • match
    • 设置为全局匹配后可以返回全部内容的分组,而不能匹配所有小正则分组
    • match 设置非全局匹配时候,与 exec 效果相同
  • exec
    • exec 执行可以匹配到目标字符串中第一个匹配对象,并返回以 0 号位为大匹配对象 2 号位为小匹配对象的数组
    • 其中 exec 执行会保存 index,确定下次开始匹配的位置
    • 可以使用?:阻止分组捕获,只匹配不捕获
  • test
    • 如果 reg 设置全局匹配,test 会修改 reg 的 lastIndex 的值,会对匹配产生影响

查看评论