1

前言

笔记,以及一些额外的补充

什么是JavaScript

JavaScript的实现

JavaScript约等于ECMAScript,但JavaScript不限于ECMAScript。JavaScript包含了ECMAScript,DOM,BOM

ECMAScript

ECMAScript,即ECMA-262定义的语言,并不局限于Web浏览器。ECMA-262将这门语言作为一个基准来定义,以便在它之上再构建更稳健的脚本语言。Web浏览器只是ECMAScript实现可能存在的一种宿主环境。宿主环境提供ECMAScript的基准实现和与环境自身交互必需的扩展。扩展(比如 DOM)使用ECMAScript核心类型和语法,提供特定于环境的额外功能。其他宿主环境还有服务器端JavaScript平台Node.js。

ECMAScript只提供了定义,并没有提供具体的实现。具体的实现,是由不同的宿主环境实现的,比如浏览器和Node。,也可以这样说,JavaScript实现了ECMAScript,Adobe Flash同样实现了ECMAScript。

DOM

文档对象模型,是一个应用编程接口(API)。DOM将整个页面抽象为一组分层节点

BOM

浏览器对象模型(BOM) API,用于支持访问和操作浏览器的窗口。使用BOM,开发者可以操控浏览器显示页面之外的部分。

HTML中的JavaScript

script元素

script元素的常用的属性

  • async,
  • crossorigin,默认不使用CORS,
  • defer,script会立即下载,但是标签会延迟到整个页面完毕后才运行
  • src,外部script文件的地址
  • type,按照惯例这个属性默认是text/javascript, 如果这个是属性是module
  • nonce,控制是否允许内联js的加载
  • nomodule,如果当前环境不支持es6模块,才会执行该script

script的执行,可以使用内联js代码,或者外部加载。对于内联的js代码,代码中不能出现</script>字符串,否则浏览器会解析错误。解决办法是对字符串进行转义成"</script>"

加载外部js文件,可以使用src属性设置外部文件的地址。

无论是外部js文件,还是内联js都会阻塞页面。包含了解释器解析的时间,对于外部的js还包括了下载js文件的时间。

浏览器会按照<script>在页面中出现的顺序依次解释它们,前提是它们没有使用 defer 和 async 属性。第二个<script>元素的代码必须在第一个<script>元素的代码解 释完毕才能开始解释,第三个则必须等第二个解释完,以此类推。(js文件下载是并发下载的,但是解释是按照顺序解释的

如果不添加defer或者async,script的下载过程是会阻塞html的渲染的。虽然现在的浏览器支持js文件的并发下载,但是不代表不会阻塞html的渲染(而且之前的浏览器是不支持浏览器并发下载js文件的)

script load

https://segmentfault.com/q/10... 这篇回答比较好

对于控制其他源的js加载

红宝书中没有给出很好的方法。但是可以使用CSP,控制浏览器加载的源的。

https://juejin.cn/post/685457...

关于crossorigin属性

script的加载标签是不受同源限制的,crossorigin属性是为了捕获非同源的脚本抛出的错误

https://juejin.cn/post/688454...

关于nonce属性

https://juejin.cn/post/685457...

关于nomodule属性

拥有nomodule属性的script不会在支持es6模块的环境下执行

标签位置

通常把script标签放到body的最后面,避免加载大型js文件时,页面长时间的白屏

推迟执行脚本

defer属性,script会立即,并行下载,但是标签会延迟到整个页面完毕后才运行。defer属性,只对外部的js文件有效。defer会保证js执行的顺序。

异步执行脚本

async属性和defer属性类似,都是立即,并行下载,但是与defer属性不同的是,async属性不能保障script标签是按照顺序执行的。

动态加载脚本

使用DOM API,动态创建script标签。通过这种形式创建的默认是异步加载的,相当于添加了async属性,如果不想拥有这样的默认特性,可以强制设置为同步加载。

let script = document.createElement('script');
script.src = 'gibberish.js';
script.async = false;
document.head.appendChild(script);

通过这种形式加载的js文件,浏览器是不可见的,可以使用预加载进行优化

<link rel="preload" href="动态加载js文件的地址">

noscript元素

<noscript> 元素出现,被用于给不支持JavaScript的浏览器提供替代内容(Safari浏览器可以关闭js代码的执行)

语言基础

语法

严格模式

严格模式需要在代码的开头添加"use strict"预处理指令,ECMAScript3的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。"use strict"也可以只添加到一个函数的内部,函数内部将会是严格模式。

语句

分号不是必须的,但是最好不要省略。加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。

ps: 关于分号是否省略,我觉得还是不要省略,我之前遇到过一个例子👇。如果不在const a = 1后面加分号,会导致解析器认为1是一个函数,抛出VM51:2 Uncaught TypeError: 1 is not a function的错误。

// Uncaught TypeError: 1 is not a function
const a = 1
(() => {
    console.log('IFEE')
})()

// ok!
const a = 1;
(() => {
    console.log('IFEE')
})();

变量

var

使用var操作符定义的变量会成为包含它的函数的局部变量。在使用var声明变量时,声明的变量会自动提升到函数作用域顶部(但是优先级小于函数声明)。使用var重复声明同一个名称的变量也是没有问题的。


function foo() {
    // 不会报错,var会有声明提升
    console.log(age);
    var age = 26;
}

// 等价于
function foo() {
    var age;
    console.log(age);
    age = 26;
}
foo();
let

let和var类似,但是最明显的区别是。var的作用域范围是函数,而let作用域范围是块。let也不允许在同一个作用域内重复声明。let也不会存在声明自动提升,在声明前无法访问变量。

使用let声明的变量,也不会成为window的属性。

for循环中的let

使用let后,迭代变量的作用域仅限于for循环块内部

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

for (let i = 0; i < 5; ++i) {
}
console.log(i); // ReferenceError: i 没有定义
// 所有的 i都是同一个变量,因而输出的都是同一个最终值
for (var i = 0; i < 5; ++i) {
    // 5, 5, 5, 5, 5
    setTimeout(() => console.log(i), 0)
}

// 使用let声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。 每个 setTimeout 引用的都是不同的变量实例
for (let i = 0; i < 5; ++i) {
    // 0, 1, 2, 3, 4
    setTimeout(() => console.log(i), 0)
}
const

const的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。同样不允许重复声明,也存在暂存性死区。const作用域范围也是块。

const声明的限制只适用于它指向的变量的引用。换句话说,如果const变量引用的是一个对象, 那么修改这个对象内部的属性并不违反 const的限制。

const不能用于for循环,因为会存在变量的自增。

// error
for (const i = 0; i < 10; ++i) {}

// ok
for (const j = 7; i < 5; ++i) {
    console.log(j);
}

数据类型

ECMAScript中有6种简单的数据类型,Undefined、Null、Boolean、Number、 String、Symbol。一种复杂的数据类型Object(函数在ECMAScript也被认为是对象)。

Undefined类型

Undefined类型只有一个值,就是undefined。当使用var或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值。注意等于undefined的变量,和未声明变量的区别。

使用typeof时,undefined会返回undefined,未声明变量同样会返回undefined。

let name;
console.log(typeof name); // undefined 
console.log(typeof age); // undefined
Null类型

Null类型同样只有一个值,即null。null值表示一个空对象指针,所以typeof null会返回"object"。undefined值是由null值派生而来的。所以null == undefined返回true。

null和undefined有关系,但是作用却完全不同。null常用于初始化变量。

Boolean类型

Boolean类型,有两个字面值true和false。虽然布尔值只有两个,但所有其他ECMAScript类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以使用Boolean(),Boolean()可以在任意类型的数据上调用,而且始终返回一个布尔值。

Boolean(0) // false
Boolean('') // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false

if等流控制语句会自动执行其他类型值到布尔值的转换

Number类型
// 10进制
let num = 55;
// 16进制
let num3 = 0x1f

科学记数法

// 等于31250000, 3.125 作为系数,乘以 10 的 7 次幂
let num1 = 3.125e7; 
// 等于0.000 000 3 
let num2 = 3e-7;

ECMAScript由于使用了IEEE754数值,所以浮点数计算存在偏差,例如,0.1加0.2得到的不等于0.3。(这种错误并非ECMAScript 所独有。其他使用相同格式的语言也有这个问题)

如何避免这种精度问题?

可以将浮点数变为整数后,计算。然后再转换为浮点数。

获取使用现成的类库 https://medium.com/javascript...

值的范围
  • Number.MIN_VALUE 最小值
  • Number.MAX_VALUE 最大值

如果某个计算得到的数值结果超出了范围,那么这个数值会使用Infinity或者-Infinity表示。Infinity和-Infinity不能在做进一步的操作。

可以使用isFinite(num), 判读数值是否在范围内

NaN

不是数值(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。

console.log(0/0); // NaN
console.log(-0/+0); // NaN

NaN的特性

  • NaN不等于任何值,包括自身
  • NaN与任何值进行计算都会返回NaN
isNaN

isNaN用于判读参数是否"不是数值",如果不是返回true,如果是返回false/

isNaN,会尝试把它转换为数值。比如:true -> 1, "100" -> 100, 不能转换为数值的值都会导致这个函数返回 true

console.log(isNaN(NaN));  // true
console.log(isNaN(10));  // false
console.log(isNaN("10")); // false
console.log(isNaN("blue"));  // true
console.log(isNaN(true)); // false
数值转换

Number, 空字符串,null返回0,undefined返回NaN,不能转为数字的字符串返回NaN

parseInt, 空字符串返回NaN,对于字符串parseInt会尽可能转换,比如parseInt("1234blue")返回1234,parseInt("blue1234")则返回NaN,浮点数会返回整数的部分。parseInt也拥有第二个参数,第二个参数的含义是用于指定解析的进制数。

// 使用16进制,解析字符串
console.log(parseInt("0xAF", 16)) // 175
// 使用10进制,解析字符串
console(parseInt("0xAF")) // NaN
alert(parseInt(10, 2)) // 2
alert(parseInt(10, 8)) // 8
alert(parseInt(10, 10)) // 10
alert(parseInt(10, 16)) // 16

parseFloat和parseInt类似,parseFloat只能解析10进制,会忽略第二次出现的小数点。如果参数没有小数点则会返回整数。

String类型

ECMAScript中的字符串是不可变的,要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。

toString()

toString方法可以将一个值转为字符串, toString()方法可见于数值、布尔值、对象和字符串值。

let foundAs = true
let foundAsString = found.toString();  // 'true'

toString方法一般不接受参数,但是当调用对象是数字时除外,toString接受的参数,可以进数字转为指定进制的字符串。


let num = 10; 
console.log(num.toString());  // "10"
console.log(num.toString(2));  // "1010"
console.log(num.toString(8));  // "12"
console.log(num.toString(10));  // "10"
console.log(num.toString(16)); // "a"
String()

调用String()方法,会首先尝试调用对象的toString方法,如果对象没有toString方法。比如null、返回'null
', undefined返回'undefined'。

模版字符串

使用模版字符串时需要注意缩进

// 这个模板字面量在换行符之后有 25 个空格符 
let myTemplateLiteral = `first line
                         second line`;
模板字面量标签函数

模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。

let a = 6;
let b = 9;

// 可以自定义标签如何组合的行为
function simpleTag(strings, ...expressions) {
    console.log(strings);
    for(const expression of expressions) {
        console.log(expression);
    }
    return 'foobar';
}

let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;


// ["", " + ", " = ", ""] simpleTag的第一个参数
// simpleTag剩余的参数
// 6
// 9
// 15
console.log(taggedResult); // "foobar"
原始字符串

使用 String.raw 标签函数,可以获得原始的字符串内容,而不是被转义后的字符串内容

console.log(String.raw`\u00A9`); // \u00A9, 不会对\u00A9转义
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line",不会对\n转义
Symbol类型

Symbol(符号)是ECMAScript6新增的数据类型。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

基本使用
// 创建Symbol
let sym = Symbol();
console.log(typeof sym); // symbol
// 也可以额外的添加一段描述,这段描述和生成的Symbol无关
let fooSymbol = Symbol('foo');
全局符号注册表

使用Symbol.for创建符号,它会检查全局运 行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同 字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号 
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true


let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false

Symbol.keyFor可以用来查询全局符号注册表,Symbol.keyFor接收全局符号作为参数,如果存在返回全局符号的键,如果不存在返回undefined。

let s = Symbol.for('foo');
// foo
console.log(Symbol.keyFor(s));

let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。

let s1 = Symbol('foo')
let s2 = Symbol('bar')

let o = {
    [s1]: 'foo val'
};

Object.defineProperty(o, s2, {value: 'bar val'});

console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val}

Object.getOwnPropertyNames()会返回对象常规属性的数组, bject.getOwnPropertySymbols()返回对象实例的符号属性数组, 这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象,Reflect.ownKeys()会返回两种类型的键

let s1 = Symbol('foo')
let s2 = Symbol('bar')
let o = {
    [s1]: 'foo val',
    [s2]: 'bar val',
    baz: 'baz val',
    qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o)); // [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o)); // ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o)); // {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o)); // ["baz", "qux", Symbol(foo), Symbol(bar)]
Symbol.asyncIterator

作为Symbol.asyncIterator属性的方法,表示实现异步迭代器API的函数。定义了Symbol.asyncIterator属性的对象,可以被for await...of循环。(类似Symbol.iterator的异步版本)

const obj = {};
obj[Symbol.asyncIterator] = async function*() {
    yield "1";
    yield "2";
    yield "3";
};
(async () => {
    for await (const item of obj) {
        // 1, 2, 3
        alert(item)
    }
})();
for…await…of

for…await…of 语句创建一个循环,该循环遍历异步可迭代对象以及同步可迭代对象。该循环遍历异步可迭代对象以及同步可迭代对象,包括: 内置的 String, Array,类似数组对象 (例如 arguments 或 NodeList)。

for…await…of也可以迭代实现了Symbol.asyncIterator属性的对象.

const generatePromise = (res) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(res)
        }, parseInt(Math.random() * 1000))
    })
}
const list = [generatePromise(1), generatePromise(2), generatePromise(3)];

const init = async () => {
    for await(let item of list) {
        // 1, 2, 3
        alert(item)
    }
}
init()
Symbol.hasInstance

Symbol.hasInstance属性表示一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof操作符使用。instanceof操作符可以用来确定一个对象 实例的原型链上是否有原型。

Symbol.hasInstance属性定义在Function的原型上,因此默认在所有函数和类上都可以调用。我们可以重写这个方法。


class Bar {}
let bar = new Bar();
console.log(bar instanceof Bar); // true

class Baz {
    static [Symbol.hasInstance]() {
        return false;
    }
}
let baz = new Baz();
baz instanceof Baz; // false
Symbol.isConcatSpreadable

这个符号作为一个属性表示, 一个布尔值,如果是true,则意味着对象应该用 Array.prototype.concat()打平其数组元素。
如果是false,整个对象被追加到数组末尾。

如果是类数组对象,默认会添加到数组的末尾,如果设置Symbol.isConcatSpreadable属性设置为true类数组对象会像数组一样被链接到前一个数组。

let initial = ['foo'];
let array = ['bar'];
initial.concat(array); // ['foo', 'bar']

array[Symbol.isConcatSpreadable] = false
initial.concat(array) // ['foo', Array(1)]
let initial = ['foo'];
// 类数组对象
let arrayLikeObject = { length: 1, 0: 'baz' };
initial.concat(arrayLikeObject) // ['foo', {...}]


arrayLikeObject[Symbol.isConcatSpreadable] = true;
initial.concat(arrayLikeObject); // ['foo', 'baz']
Symbol.iterator

Symbol.iterator作为一个属性表示,一个方法,该方法返回对象默认的迭代器。由for-of语句使用。Symbol.iterator()会返回实现了迭代器API的对象(类似 { done: true, value: 'data' }

const obj = {};
obj[Symbol.iterator] = function*() {
    yield "1";
    yield "2";
    yield "3";
};
(() => {
    for (const item of obj) {
        // 1, 2, 3
        alert(item)
    }
})()
Symbol.match

Symbol.match作为一个属性表示一个正则表达式方法,该方法用正则表达式去匹配字符串。由String.prototype.match()方法使用。String.prototype.match()方法会使用以Symbol.match为键的函数来对正则表达式求值

'foobar'.match(/bar/); // ["bar", index: 3, input: "foobar", groups: undefined]

// 重写match方法
class FooMatcher {
    static [Symbol.match](target) {
        return false
    }
}
// 会使用重写后的match方法
'foobar'.match(FooMatcher) // false
Symbol.replace

Symbol.replace作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由String.prototype.replace()方法使用”String.prototype.replace()方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。

'foobarbaz'.replace(/bar/, 'qux'); // 'fooquxbaz'

// 重写replace方法
class FooReplacer {
    static [Symbol.replace](target, replacement) {
        return 'HelloWorld'
    }
}
'barfoobaz'.replace(FooReplacer, 'qux'); // HelloWorld
Symbol.search

Symbol.search作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()方法使用”。String.prototype.search()方法会使用以Symbol.search为键的函数来对正则表达式求值。

'foobar'.search(/bar/) // 3

class FooSearcher {
    static [Symbol.search](target) {
        return 0;
    }
}
'foobar'.search(FooSearcher) // 0
Symbol.species

对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。

class MyArray extends Array {}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray // true
c instanceof MyArray // true

b和c虽然是数组的方法返回的,但是不是Array的实例,而是MyArray的实例。如果定义了Symbol.species属性,创建衍生对象时就会使用这个属性返回的函数,作为构造函数。

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}

const a = new MyArray();
const b = a.map(x => x);

b instanceof MyArray // false
b instanceof Array // true

它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。

Symbol.split

Symbol.split作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用”。String.prototype.split()方法会使用以Symbol.split为键的函数来对正则表达式求值。

split方法会把传入非正则表达式值会被转换为RegExp对象。定义类的Symbol.split的静态函数,从而让split()方法使用非正则表达式实例。

'foobarbaz'.split(/bar/) // ['foo', 'baz']

class FooSplitter {
    // 覆盖split的行为
    static [Symbol.split](target) {
        return target.split('a');
    }
}
'foobarbaz'.split(FooSplitter) // ["foob", "rb", "z"]
Symbol.toPrimitive

Symbol.toPrimitive作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象操作使用“。很多内置操作都会尝试强制将对象转换为原始值。

class Foo {}
let foo = new Foo();

// 将foo转为原始值过程中出现错误
3 + foo // 3[object Object]
3 - foo // NaN
// 覆盖对象转为原始值的行为
class Foo {
    constructor () {
        this[Symbol.toPrimitive] = function(hint) {
            switch (hint) {
                case 'number':
                    return 3;
                case 'string':
                    return 'string bar';
                case 'default':
                    default:
                return 'default bar';
            }
        }
    }
}

let foo = new Foo();

console.log(3 + bar); // "3default bar" 
console.log(3 - bar); // 0 
console.log(String(bar)); // "string bar"
Symbol.toStringTag

Symbol.toStringTag作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用”。

class Foo {}
let foo = new Foo();
foo.toString() // "[object Object]"

class Foo {
     constructor() {
        this[Symbol.toStringTag] = 'Hello World';
    }
}
let foo = new Foo();
foo.toString()  // "[object Hello World]"
Object类型
ECMA-262 中对象的行为不一定适合 JavaScript 中的其他对象。比如浏 览器环境中的 BOM 和 DOM 对象,都是由宿主环境定义和提供的宿主对象。而宿主对象 不受 ECMA-262 约束,所以它们可能会也可能不会继承 Object。

ECMAScript中的Object也是派生其他对象的基类。Object类型的所有属性和方法在派生的对象上同样存在。

  • constructor, 构造函数
  • hasOwnProperty,用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty("name"))或符号
  • isPrototypeOf(object),用于判断当前对象是否为另一个对象的原型
  • propertyIsEnumerable(propertyName),用于判断给定的属性是否可以使用
  • toString,返回对象的字符串表示。
  • valueOf,返回对象对应的字符串、数值或布尔值表示。

操作符

位操作符

之前的文章:👇

https://juejin.cn/post/684490...

指数操作符

ECMAScript7,新增了指数操作符,和Math.pow(底数,幂数)结果一样

3 ** 2 // 9
逗号操作符

逗号操作符可以用来在一条语句中执行多个操作

let num1 = 1, num2 = 2, num3 = 3;
let num = (5, 1, 4, 8, 0); // num等于0

语句

with

with语句的用途是将代码作用域设置为特定的对象


let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;

// 使用with语句,将当前的作用域设置为location
with(location) {
    let qs = search.substring(1);
    let hostName = hostname;
    let url = href;
}

变量,作用域与内存

原始值和引用值

ECMAScript包含了原始值和引用值,原始值是按值访问的,引用值是存储在内存中的对象,js不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身。

原始值大小固定,因此保存在栈内存上。引用值是对象,存储在堆内存上(指针保存在栈内存上)。

动态属性

引用值而言,可以随时添加、修改和删除其属性 和方法。原始值不能有属性,尽管尝试给原始值添加属性不会报错。原始类型如果使用new关键字,js创建一个obj类型的实例,是可以添加属性的。

复制值

  • 原始值复制,这两个变量可以独立使用,互不干扰。
  • 引用值复制,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针。

image.png

image.png

传递参数

ECMAScript中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样(复制的是指针)。

什么是按引用传递?什么是按值传递?
  • 按引用传递,值在内存中的位置会被保存在一个局部变量
  • 按值传递,值会被复制到一个局部变量

确定类型

typeof 常用于判读原始值。但是对引用类型用处不大。对于引用类型,ECMAScript提供了instanceof操作符进行判读。

person instanceof Object // 变量person是Object吗?
colors instanceof Array // 变量 colors 是 Array 吗?

执行上下文与作用域 && 作用域链增强

执行上下文与作用域,作用域链增强,更好的解释如下👇:

https://juejin.cn/post/689551...

变量声明

严格来讲,let在JavaScript运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的缘故,实际上不能在声明之前使用 let变量。(应该是指进入执行上下文阶段let声明的变量也会VO/AO对象上初始化为undefind, 但是由于暂时性死区,无法在声明之前使用)

🚮 垃圾回收

回收策略: 标记清理

当进入上下文,遇到声明变量时,这个变量会被加上存在于上下文中的标记。如果代码运行在上下文,就不应该释放这些变量的内容。当离开上下文时,会被添加离开上下文的标记。

标记的方法会有很多种。所有变量都会在内存中被标记。垃圾回收程序会将将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内 存清理,销毁带标记的所有值并收回它们的内存。

回收策略: 引用计数

对每一个值记录引用的次数。声明变量并给它赋一个引用值时,这个值的引用数记为1。如果同一个值又被赋给另一个变量,那么引用数加1。如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。

引用计数会遇到循环引用的问题,导致引用记数永远不会为0。Netscape在4.0版放弃了引用计数的策略。

性能

过于频繁的运行垃圾回收程序,可能会影响到网页的性能。V8运行策略是“在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃圾回收”。

内存管理

因为浏览器分配的内存较少。如果数据不再必要,那么把它设置为null,从而释放其引用。这也可以叫作解除引用。这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用。

使用let,const提升性能

let,const也会提升页面性能,因为它们都是块作用域,所以相比于使用var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。

隐藏类和删除操作

V8在将解释后的JavaScript代码编译为实际的机器码时会利用“隐藏类”。


function Article() {
    this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article();

a1,和a2共享隐藏类。因为这两个实例共享同一个构造函数和原型。如果此时动态的添加a1或者a2的属性,两个Article实例就会对应两个不同的隐藏类。解决方法是,在构造函数中,声明出所有可能使用到的属性,避免动态属性的赋值。

使用delete操作符和动态添加属性导致的后果一样,将会导致实例不再共享一个隐藏类。最佳实践是把不想要的属性设置为null。

内存泄漏

以下操作可能会导致内存泄漏:

  1. 全局变量的使用
  2. 闭包的过度使用
  3. 不被清理的定时器

参考


已注销
518 声望187 粉丝

想暴富