JavaScript(一)基本语法

本文最后更新于:6 个月前

6种基本数据类型、变量、语句、执行上下文、作用域与作用域链、变量/活动对象、this的指向分析、执行上下文的变化过程

JavaScript (一)基本语法

一、基本数据类型

1、6种基本类型

  • number(数字),包括int、float、infinite、NaN,特殊NaN值,自己不等于自己,要通过isNaN()判断
  • string(字符串),可以用单引号、可以用双引号
  • boolean(布尔值),true和false
  • null(空值),表示空对象指针,null转为数字时自动变成 0
  • undefined(未定义的值),表示未初始化的变量,用let和var声明但未赋值时,就是undefined
  • symbol(符号),是ES6新增的,符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险

以上参考自《JavaScript高级程序设计》,注意:object不归为基本数据类型

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
31
32
33
34
35
36
37
38
39
typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'
null == undefined //true
null === undefined //false

5+null //0
5+undefined //NaN
NaN+1 //NaN

1+'2' //'12'
'1'+2 //'12'
+'1'+2 //3
1+2+'3' //'33'
'1'+2+3 //'123'

!undefined //true
!null //true
!0 //true
!NaN //true
!"" //true
!'' //true
!{} //false
![] //false

undefined == false //false

NaN == NaN //false
1 === 1.0 //true
0.1 + 0.2 === 0.3 //false
0.3/0.1 //2.9999999999999996

let sym = Symbol(); //Symbol的定义方法,不能使用new关键字

2、Object类型(不是基本类型)

每个Object都有如下属性和方法:

  • **toLocaleString()**:返回对象的字符串表示,该字符串反映对象所在的本地化执行环境
  • **toString()**:返回对象的字符串表示
  • **valueOf()**:返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同
  • constructor:用于创建当前对象的函数
  • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性
  • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型
  • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in语句枚举

因为Object是所有对象的基类,所以任何对象都有以上这些属性和方法

3、String类型

在这里着重探究String的一些性质与方法

  • 不限单、双引号,如果字符串内部有引号,要加转义字符\作为前缀
  • 多行字符串,一种方式是用\换行,另一种是ES6新加的,反引号`
  • 字符串之间可以通过+拼接,也可以通过反引号 hello ${name} 反引号的方式插入字符串变量
  • s[0]这种赋值方式不起作用,不会改变字符串内容
方法 说明
toUpperCase()
toLowerCase()
返回全部大写、小写
substring(start,end)
slice(start,end)
substr(start,length)
接受两个参数,表示截取的区间 [起点,终点),第二个参数可选,缺省默认为字符串结尾
返回一个新的字符串,截取后的字符串。substring将负数值视为0,slice和substr将负数视为从结尾开始倒数
indexOf(字符)
lastIndexOf(字符)
分别从字符串开头寻找字符、从字符串末尾开始寻找字符。它们都可以接受一个或者两个参数,第一个参数是寻找的字符,第二个参数是开始寻找的起点。若没找到,返回-1
startsWith(字符串)
endsWith((字符串)
includes((字符串)
接受一个字符串参数,返回boolean值,表示是否以指定字符串开始、结尾、或包含指定字符串。接受第2个参数,表示开始搜索的位置
trim()
trimLeft()、trimRight()
返回去除头尾空格后的副本
repeat(数值) 将字符串复制xx次,返回复制后的副本
padStart(数值,字符串)
padEnd(数值,字符串)
用另一个字符串填充当前字符串 (如果需要的话,会重复多次),以便产生的字符串达到给定长度。从当前字符串左侧开始填充。第二个参数可选,表示填充的字符(串),缺省默认为空字符
如果第一个参数比实际长度短,返回原字符串。
padEnd从字符串右侧开始填充
match() 与RegExp对象的exec方法一样,接受一个正则表达式或者RegExp对象,返回一个Array数组
search() 接受正则表达式,返回第一个匹配的下标。若没找到,返回-1
replace(字符串,字符串) 第一个参数接受字符串、或者正则表达式,表示需要被替换的字符串,如果是字符串那么只替换第一个匹配项,如果要替换所有需要使用正则表达式且加上g标记
第二个参数接受字符串,表示替换后的字符串。第二个参数还可以接受一个函数用于处理(JavaScript replace() 方法 (w3school.com.cn)
返回一个新的替换后的字符串,原来的字符串不变
split() 第一个参数接受一个分割字符或者正则表达式,第二个参数可选,接受一个数字,限制返回数组的大小。返回值是一个分割后的字符串数组
charAt(下标)
charCodeAt(下标)
返回字符串对应下标位置的字符
返回字符串对应下标位置的字符的Unicode编码

需要注意:以上的这些方法并非String原始值类型本身所有,而是在调用这些方法的时候,JavaScript会在后台自动完成String包装类型的创建、方法调用、包装对象销毁的一系列操作。

1
2
3
4
let str = "foo";
str.padStart(6); //' foo'
str.padStart(6,'*') //'***foo'
str.padStart(6,'&*')//'&*&foo'
1
2
let message = 'abcde'
[...message] // ['a', 'b', 'c', 'd', 'e']
1
2
3
//slice(start,end)的区间范围,为[start,end),即“左闭右开”区间
var str = '/a/b/ca'
var res = str.slice(0,4) //'a/b'

补充:string与number之间的自动转换

1
2
3
1”+1 :“11
+“1”+1 :2 (+“1” 先变成了数字类型)
1+“1”:“11

4、number类型

十进制转二进制

1
2
var num = 12
var result = num.toString(2);
1
2
3
4
5
6
var num = 12.5678
Math.ceil(num) // 13,向上取整
Math.floor(num) // 12,向下取整
Math.round(num) // 13,四舍五入
num.toFixed(2) // 12.57,固定小数点位数(四舍五入的方式)
num.toPrecision(3) // 12.6,固定长度(四舍五入的方式)
1
Math.abs(12-13)	// 1

二、变量

1、变量声明

var let const
版本支持 所有ES版本 ES6之后 ES6之后
声明作用域 函数作用域 块作用域 块作用域
变量声明提升 不会 不会
同一作用域内重复声明 允许 不允许 不允许
必须声明的同时初始化
不能声明迭代变量(如:for循环的i)
1
2
3
4
5
6
7
8
9
10
11
/******** var变量是函数作用域 *******/
var val = 12
function fun1(){
console.log(val)
}
fun1()
// 12
// 分析,在函数上下文中没有val变量,所以会沿着作用域链向上寻找,找到了全局上下文的变量对象中的val

for(var i=0;i<3;i++){}
console.log(i) // 3
1
2
3
4
5
6
7
8
9
/******** var具有声明提升 *******/
console.log(val)
var val = 20
// undefined

/******** let没有声明提升 *******/
console.log(val)
let val = 20
// Uncaught SyntaxError: Identifier 'val' has already been declared
1
2
3
4
5
6
7
8
9
10
/******** var变量作用域 和 声明提升的探究 *******/
var val = 12
function fun(){
console.log(val)
var val = 20
console.log(val)
}
fun()
// undefined 20
// 分析,在函数上下文中声明了val(声明提升),但是没有赋值定义,所以为undefined
1
2
3
4
5
6
7
8
9
10
11
12
// for循环的特别之处,设置循环部分是一个父作用域,循环体内部是另外一个单独的子作用域
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// 输出3次abc

for (var i = 0; i < 3; i++) {
var i = 'abc';
console.log(i);
}
// abc (只输出1次,因为修改了i,使循环条件不成立了)

暂时性死区,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”

1
2
3
4
5
6
7
// 只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc';
let tmp;
}
// Uncaught ReferenceError: Cannot access 'tmp' before initialization
1
2
3
4
5
6
// 不报错
var x = x;

// 报错
let x = x;
// ReferenceError: x is not defined

2、块级作用域

ES6之前,只有 全局作用域函数作用域,ES6随着letconst的新增,增加了 块级作用域这个概念。

函数作用域使得一些不合理的现象存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 不合理现象一:内层变量覆盖外层变量
var tmp = new Date();
function f() {
console.log(tmp); // 这里的tmp被f函数作用域中,tmp的变量声明提升给覆盖了,所以获取不到外层tmp的值
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined

function f(){
var tmp = 1
if(true){var tmp = 2}
console.log(tmp)
}
f() // 2
1
2
3
// 不合理现象二:循环变量泄露为全局变量
for(var i=0;i<3;i++){}
console.log(i) // 3

块级作用域必须有大括号,如果没有大括号,被认为不存在块级作用域

1
2
3
if (true) let x = 1		// 报错

if (true) {let x = 1} // 不报错

内层作用域可以定义外层作用域的同名变量

1
2
3
4
5
let num = 1
if(true){
let num = 2
}
// 不报错

内层作用域变量不会覆盖外层作用域变量,同时也意味着内层作用域定义的变量在外层不可用

1
2
3
4
5
6
7
8
9
10
11
12
function f(){
let tmp = 1
if(true){let tmp = 2}
console.log(tmp)
}
f() // 1,如果将两个let替换为var的话,结果将输出2

function f(){
if(true){let tmp = 2}
console.log(tmp)
}
f() // 报错

块级作用域内部,优先使用函数表达式

1
2
3
4
5
6
{
let a = 'secret';
let f = function () {
return a;
};
}

块级作用域的出现,使得ES5 广泛使用的 「匿名立即执行函数」不再必要了

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
/******** 经典的函数闭包问题 *******/
// 变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,JS引擎内部记住了每轮循环的值
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]() // 6

// 但是使用var声明时,表现就有所不同了,JS引擎只记住了最后的i值
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]() // 10,并不是期望出现的6

// 在let出现之前,都是像下面这样通过函数闭包解决这种问题的
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function (n) {
return ()=>{console.log(n)}
}(i);
}
a[6]() // 6

3、原始值&引用值

原始值(primitive value)就是最简单的数据,引用值(reference value)则是由多个值构成的对象。

JavaScript有6种原始值(基本数据类型):Number、String、Boolean、Null、Undefined、Symbol。保存原始值的变量是按值访问的

JavaScript不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。保存引用值的变量是按引用访问的

4、判断变量类型

  • typeof操作符,用来区分各种原始类型(string、number、boolean、undefined)
  • instanceof操作符,用来区分各种引用类型

5、解构赋值

从ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值

1
2
3
var [x,y,z] = [1,2,3];  // 同时给x、y、z赋值,变量要用[]括起来
var [x,[y,z]] = [1,[2,3]]; // 结构对应即可
var [,,z] = [1,2,3]; // 可以忽略一些元素
1
2
3
4
5
6
7
8
9
10
11
12
13
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school',
address: {
city: 'Beijing',
street: 'No.1 Road',
zipcode: '100001'
}
};
var {name,address:{city,zip}} = person; // 可以直接获取对应的属性,要保证名字和结构对应,zip为undefined,因为名字对不上

如果要使用不一样的变量名字,可以使用下面这种方式

1
2
var {name,passport:id} = person;
id //'G-12345678'

解构赋值还可以定义默认值,这样可以避免undefined

1
2
var {name,single=true} = person;
single //true

注意写法

1
var {x,y} = {x:1,y:2} 正确
1
2
var x,y;
{x,y} = {x:1,y:2}; // 报错!{x,y}被当成了代码块
1
2
var x,y;
({x,y} = {x:1,y:2};) 正确

使用场景

如果函数接受一个对象作为参数,那么可以直接用解构赋值的方式把对象的属性写在参数里面

1
2
3
4
5
function buildDate({year,month,day,hour=0,minute=0,second=0}){
return new Date(year + ' -' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}
var Date = {year:2022,month:4,day:13};
buildDate(Date);

三、语句

JavaScript的语句包括:if语句、while语句、do-while语句、for语句、for-in语句、for-of语句、标签语句、break和continue语句、with语句、switch语句,其中大多数语句与C++中的语法和使用一致。下面详细介绍一下for-in语句、for-of语句和with语句

1、for-in语句

for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:

1
for(property in expression) {statement;}

使用for in会遍历数组所有的可枚举属性,包括原型,如果不想遍历原型方法和属性的话,可以在循环内部判断一下,使用hasOwnProperty()方法

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = ['A', 'B', 'C'];
for (var i in a) {
console.log(i); // '0', '1', '2'
console.log(a[i]); // 'A', 'B', 'C'
}

var person = {
name: "xiaoming",
age: 18
}
for(cosnt item in person){
console.log(item); // name age 而不是”xiaoming" 18
}
  • 注意:for-in遍历的数组的下标,而不是数组元素

2、for-of语句

for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素不能遍历对象,因为对象没有迭代器对象。语法如下:

1
for(property of expression) {statement;}

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = ['A', 'B', 'C'];
for (let i of a) {
console.log(i); // A B C,这里不能使用const声明i,使用const声明i会报错
}
for(const i of ['A','B','C']){
console.log(i); // A B C,这里使用const声明i不会报错,因为迭代的是一个const数组
}

var person = {
name: "xiaoming",
age: 18
}
for(const item of person){
console.log(item); //抛出语法错误,因为person对象不是iterable类型,具体原因查看《迭代器和生成器》一节
}
1
2
3
4
5
let arr = ['a','b','c']

for (let [index,item] of arr.entries()){
console.log(index,item)
}

3、with语句

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

1
with(expression){statement;}

实例:

1
2
3
4
5
6
7
8
9
10
11
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;

改进为:

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

四、执行上下文

变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variableobject),而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它

1、执行上下文

执行上下文可以理解为当前代码的运行环境。在 JavaScript 中,运行环境主要包含了全局环境函数环境

上下文中的代码在执行的时候,会创建变量对象的一个作用域链,它决定了各级上下文中的代码在访问变量和函数时的顺序。作用域链是按照”栈“LIFO的特性添加的,如下图是一个形象的作用域链表示

image-20220510164738557

注意:当执行一个函数时(而非定义函数时),才创建对应的执行上下文,全局代码一上来就执行,所以一开始就有一个全局上下文。

2、变量对象+作用域链+this

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

执行上下文的处理分为两个阶段:1.调用函数,创建执行上下文,进入执行上下文,根据参数对象Arguments初始化AO(VO)对象;2.执行函数代码,根据代码修改AO对象的值

3、执行上下文栈

在JavaScript引擎中,维护着一个执行上下文栈,当新建了一个执行上下文时,就会将它加入到栈顶;该函数代码执行完之后,将它的执行上下文从执行上下文栈中退出。

下面用一个例子来演示执行上下文栈变化的过程,ECStack(Execute Context Stack)执行上下文栈

1
2
3
4
5
6
7
8
9
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
1
2
3
4
5
6
ECStack.push(globalContext)			//首先遇到全局代码,将全局上下文加入栈
ECStack.push(<checkscope> functionContext) //执行checkscope函数,将checkscope函数的执行上下文加入栈
ECStack.push(<f> functionContext) //执行f函数,将f函数的执行上下文加入栈
ECStack.pop()
ECStack.pop()
ECStack.pop()

五、作用域&作用域链

1、变量作用域

作用域:指程序源代码中定义变量的区域(全局作用域、函数作用域、块级作用域)。 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

  • 不同关键字声明变量的作用域
var let const
声明作用域 函数作用域 块作用域 块作用域
变量声明提升 不会 不会
1
2
3
4
5
6
7
8
9
10
11
12
// 函数作用域
function foo(){
for(var i=0;i<10;i++){...}
i++ //i=12,由于var声明的变量是函数作用域(而不是块级作用域),所以在for语句之外,依然能获得i
}

// 变量声明提升,先扫描整个函数体的语句,把变量的声明提升到函数顶部
function foo(){
var x = 1 + y;
console.log(x); //结果为NaN
var y=2; //如果没有这句,将报错
} //变量声明会提升,但是赋值不会提升
  • 由于函数可以嵌套,所以内部函数可以使用外部函数的变量。如果同名变量,内部变量覆盖外部变量

  • 命名空间,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突。为了解决这个问题,可以把所有全局变量和顶层函数绑定到一个全局变量中

    1
    2
    3
    var MYAPP = {};
    MYAPP.name = "app"
    MYAPP.foo = function(){...}

2、作用域链

作用域链:当查找变量时,会先从当前执行上下文的变量对象中查找;如果没有,再从父级执行上下文的变量对象中查找;一直找到全局执行上下文的变量对象为止。这个由多个执行上下文的变量对象构成的链表就叫作用域链。

函数有一个内部属性**[[scope]]**,

  • 函数创建时就保存了所有父变量对象。
  • 函数执行时,加入当前执行上下文的变量对象
  1. 函数创建时,保存所有父变量对象到[[scope]]属性中

    1
    2
    3
    4
    5
     function foo() {
    function bar() {
    ...
    }
    }

    此时各自的[[scope]]为:

    1
    2
    3
    4
    5
    6
    7
    8
    foo.[[scope]] = [
    globalContext.VO
    ];

    bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
    ];
  2. 函数激活时(即实际调用函数时),进入函数的执行上下文,将活动对象添加到作用域链头部,此时的[[scope]]为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    foo.[[scope]] = [
    fooContext.AO,
    globalContext.VO
    ];

    bar.[[scope]] = [
    barContext.AO
    fooContext.AO,
    globalContext.VO
    ];

六、变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

1、全局上下文的变量对象

全局上下文中的变量对象就是全局对象,可以通过this引用,也可以通过Window引用,是所有全局变量的宿主(全局变量作为全局对象的属性)

2、函数上下文的变量对象

在函数上下文中,我们用**活动对象(activation object, AO)**来表示变量对象。

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,也就是活动对象上的各种属性才能被访问。

3、创建过程

活动对象是在进入函数上下文时刻被创建的。执行上下文的代码会被分成两个阶段进行处理:

  1. 分析(进入执行上下文)
  2. 执行(代码执行)

进入执行上下文,变量对象包括:函数的形参、函数声明、变量声明

1
2
3
4
5
6
7
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1)

执行foo(1),创建执行上下文,进入执行上下文,根据Arguments对象初始化,此时的AO为:

1
2
3
4
5
6
7
8
9
10
AO = {
arguments:{
0:1,
length:1
},
a:1,
b:undefined,
c:reference to function c(),
d:undefined
}

代码执行,顺序执行代码,然后根据代码修改变量对象的值,当代码执行完后,AO为:

1
2
3
4
5
6
7
8
9
10
AO = {
arguments:{
0:1,
length:1
},
a:1,
b:3,
c:reference to function c(),
d:reference to FunctionExpression "d"
}

七、this

this是执行上下文的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。

1、this指向问题

this 为调用函数的对象

1
2
3
4
5
6
function f1(){return this;}
f1() //Window,默认指向全局对象

"use strict"
function f1(){return this;}
f1() //undefined,严格模式下

在(对象)方法中,this为该方法所属的对象

1
2
3
4
5
6
7
8
9
10
prop = 26
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
o.f(); //37
var f = o.f
f() //26,此时函数f属于全局对象

在函数中,this表示全局对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myFunc(){
this.name = "Q";
return this.name;
}
myFunc() //'Q'
window.name //'Q'

//对象方法内部再定义函数,this并不指向对象,而是指向全局对象
var xiaoming = {
log:function(){
function getLog(){
console.log(this)
}
return getLog();
}
}
xiaoming.log() //Window

类上下文中,this为该实例对象,类静态方法不属于类实例对象

1
2
3
4
5
6
7
8
9
10
11
class Example {
constructor() {
const proto = Object.getPrototypeOf(this);
console.log(Object.getOwnPropertyNames(proto));
}
first(){}
static third(){}
whereThis(){return this;}
}
let e = new Example() //['constructor', 'first', 'whereThis']
e.whereThis() //Example {},指向new出来的实例

箭头函数中,this等于定义它的执行上下文的this

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
31
32
33
34
35
(()=>this)()	//Window ,全局代码中,指向全局上下文
var obj = {
bar:function(){
var x = (()=>this)
return x;
}
}
var fn = obj.bar() //返回的是箭头函数,此时的fn为"()=>this"
var fn2 = obj.bar //返回的是bar函数,在全局调用bar函数,所以bar的this指向全局对象
fn() //{bar: ƒ},fn函数的this指向obj对象
fn2()() //Window,fn2函数的this指向全局对象。分两步分析,第一步fn2(),全局对象调用了fn2,所以fn2的this指向全局对象。然后调用fn2()(),此时由于上一步,bar函数的this指向全局对象,而箭头函数的this取决于定义它的执行上下文的this,也即使bar的this,所以是全局对象

var obj = {
x:1,
bar:()=>this.x,
bar1:()=>this, //箭头函数,返回Window对象
bar2(){return this} //普通函数,返回obj对象
}
obj.bar() //undefined
var x = 2
obj.bar() //2 obj对象中的this是window对象

// 在箭头函数中,this等于定义它的执行上下文的this,不会因为是谁调用而改变,在定义的时候已经固定死了
function fn(){
this.a = 1
this.log = ()=>{console.log(this.a)}
this.log2 = function(){console.log(this.a)}
}
var f = new fn
f.log() //1
var outLog = f.log
var a = 2
outLog() //1,箭头函数的this指向定义时的上下文对象,是f实例,不会因为被Window对象调用而改变this指向
var outLog2 = f.log2
outLog2() //2,普通函数的this指向调用它的对象,这里调用outLog2的是Window对象

2、改变this指向

指定this的方式:call、apply、bind

1
2
3
4
5
6
7
function add(c, d) {
return this.a + this.b + c + d;
}
var o = {a: 1, b: 3}

add.call(o, 5, 7); //16
add.apply(o, [10, 20]); //34

在非严格模式下使用 callapply 时,如果用作 this 的值不是对象,则会被尝试转换为对象。nullundefined 被转换为全局对象(所以使用 callapply 时,如果只想传递后面的参数,可以设置第一个参数为null

bind只能绑定this一次

1
2
3
4
5
6
7
function f(){ return this.a}
let b = {a:1}
let c = {a:2}
var g = f.bind(b)
g() //1
var h = g.bind(c)
h() //1

八、执行上下文变化过程推演

下面用一段代码来演示在一段函数执行时,执行上下文(栈)、作用域链Scope、活动对象AO的变化,同样用ECStack表示执行上下文栈

1
2
3
4
5
6
7
8
9
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
  1. 执行全局代码,创建全局上下文,并压入栈

    1
    ECStack = [globalContext]
  2. 进入全局执行上下文

    1
    2
    3
    4
    5
    6
    7
    8
    globalContext = {
    VO:{
    scope:undefined,
    checkscope: reference to function checkscope(){} //函数声明
    },
    Scope: [globalContext.VO], //作用域链
    this:globalContext.VO
    }
  3. checkscope函数被创建,保存作用域链到函数的内部属性[[scope]]

    1
    checkscope.[[scope]] = [globalContext.VO]
  4. checkscope函数被创建的同时,代码执行,根据代码内容修改变量对象的值

    1
    2
    3
    4
    5
    6
    7
    8
    globalContext = {
    VO:{
    scope:"global scope",
    checkscope: reference to function checkscope(){} //函数声明
    },
    Scope: [globalContext.VO], //作用域链
    this:globalContext.VO
    }
  5. 执行checkscope函数,创建它的执行上下文,加入栈

    1
    ECStack = [checkscopeContext,globalContext]
  6. checkscope函数执行上下文初始化:

    1. 复制checkscope函数的[[scope]]属性创建作用域链
    2. 用arguments创建活动对象
    3. 初始化活动对象,即加入形参、函数声明、变量声明
    4. 将活动对象压入checkscope作用域顶端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    checkscopeContext = {
    AO:{
    arguments:{length=0},
    f:reference to function f(){}, //函数声明
    scope: undefined, //变量声明,然后在执行代码的过程中,被修改为实际值"local scope"
    },
    Scope:[AO,globalContext.VO],
    this:undefined //猜测可能是函数执行的时候才获得
    }
  7. f被创建,保存作用域链到函数的内部属性[[scope]]

    1
    f.[[scope]] = [checkscope.AO, globalContext.VO]
  8. checkscope函数执行完毕,它的执行上下文从栈中退出

    1
    ECStack = [globalContext]
  9. 执行f函数,创建它的执行上下文,加入栈

    1
    ECStack = [fContext, globalContext]
  10. .f 函数执行上下文初始化,第一步、复制函数[[scope]]属性创建作用域链,第二步、用arguments创建活动对象,第三步、初始化活动对象,即加入形参、函数声明、变量声明(这里f函数里面没有变量也没有函数),第四步、将活动对象压入checkscope作用域顶端

    1
    2
    3
    4
    5
    6
    7
    fContext = {
    AO:{
    arguments:{length=0},
    },
    Scope:[AO, checkscope.AO, globalContext.VO],
    this:undefined
    }
  11. 查找scope变量,在当前AO活动对象中没有找到,沿着作用域链向上查找,在checkscope.AO中找到了scope。返回对应的值

  12. f函数执行完毕,它的执行上下文从栈中退出

    1
    ECStack = [globalContext]
  13. 全部代码执行完毕,全局上下文从栈中退出

    1
    ECStack = []

参考链接

mqyqingfeng/Blog: JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。 (github.com)


JavaScript(一)基本语法
http://timegogo.top/2022/05/13/JavaScript/JavaScript(一)基本语法/
作者
丘智聪
发布于
2022年5月13日
更新于
2023年9月3日
许可协议