作用域

问题

先看这段代码

1
2
3
4
5
6
7
{
a = 1;
function a() { };
a = 2;
console.log(a); // 输出:2
}
console.log(a); // 输出:1

是不是很不解???常规思路都是输出2啊。
没关系,在了解了JS块级作用域之后你就懂了

全局作用域和function

window为全局对象,在任何一个地方,如果一个变量a = 1(没有任何修饰,如var、let、const),那么也可以理解为window.a = 1
var声明的变量的作用域在funtion中,否则在上层的function,若上层没有function,那么就会延伸到window

eval('代码作用域'):默认eval执行的代码作用域同上。
但是如果在eval之前开启严格模式use strict;那么eval里面的变量不会外溢。除非显示给外部对象赋值,如eval('window.bbb=48')

Ecma5之前只有函数和全局作用域,也就是全局window或者function(){...}函数之内,而且var和function,在未声明之前可以访问,原因是js有内部变量提前的特性
在同一作用域下function函数和var声明的变量都会被提至当前作用域的顶层,var优先声明,function其次,其中function提升的同时,函数体的实现也定义了出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function test() {
b();
console.log('我是函数');
var num = 1;

function b() {
console.log('我是内嵌函数b,num:', num);
}
}
test(); // 输出:1.我是内嵌函数b,num:undefined 2.我是函数

// 解释器解释代码之后会变成如下这个样子

function test() {
var num = undefined;
var b = function () { // function提升的同时,方法实现也定义了出来
console.log('我是内嵌函数b,num:', num);
}
b();
console.log('我是函数');
num = 1;
}
test();

块级作用域

Ecma6新增了块级作用域,增加了两个变量修饰符:let(值可变,不可二次声明)和const(常量、值不可变,可二次声明),未声明之前访问会报错,而且varlet以及const声明的变量不能互换
理解了函数和变量提升之后,那么问题来了,如果块级作用域和块外作用域共有一个同名的变量,而function函数写在块中,该函数引用到同名变量,那么该函数到底是用块内还是块外变量呢?如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = 1;
{
let a = 2;
function test() {
console.log(a);
}
}
test();

// 按照上面的函数和变量的提升思路,那么这就是解释器解释后的代码样子
var a = undefined;
var test = function () {
console.log(a);
}
a = 1;
{
let a = 2;
}
test();

很明显,按照变量和函数提升至顶层之后的思路,解释之后会输出1,可我们实际想要的是2呀。输出为1就违背了块级作用域的概念,那么该如何解决呢?毕竟变量和函数的提升是老的特性,新设计的特性肯定要兼容旧的。没办法,js制定者只能在做一些取舍了
取舍如下:

  1. 函数如果在块中,那么funtionvar照样提升至前,只不过function的实现不允许提前定义,这样可以避免块中的内容溢出到块外,即块的内容只在块里面
  2. 解释器在解释到块级作用域时,如果块中有函数,那么会在块中的最初位置用let以及新的变量名,重新定义一下这个函数,因为funtionlet定义在块中了,那么该function肯定可以访问到块中的变量
    • 为什么会在块中用新的变量从新定义呢,因为一个变量不能从varlet以及const相互转换
  3. 因为块内的函数的名称变了,所以块内涉及到的老的函数名称时,也要随着变。不然用let修饰的新变量名称也没有任何意义啊~
  4. 然后又因为函数的作用域不仅仅在块内,块外也可以访问(要兼容之前的特性),所以在执行到函数原有声明的位置时,他会用var以及原有的变量名再次声明一下
    这样就解决了块外和块内的变量名一样的问题了。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var a = 1;
{
let a = 2;
function test() {
console.log(a);
}
}
test();

// 解释器解释后的代码的样子

var a = undefined; // 变量提升
var test = undefined; // 块内的函数提升,舍弃方法体的定义
a = 1;
{
let new_test = function () { // 块内的函数,用let以及新的变量名定义,并在此定义出函数实现体
console.log(a);
}
let a = 2;
var test = new_test; // 用var把原有的变量名声明一下,执行完该代码块时,函数也可以在外部访问
}
test();

但是也有问题,比如在块中定义的函数,必须执行完块时,函数才可以访问

1
2
3
4
5
6
7
// test();   //执行会报错,找不到方法
{
function test() {
console.log(123);
}
}
test(); //执行完块时才可访问

世界上没有任何东西是十全十美的,在一件大事件上要尽量争取最好的度,做出最大的兼容(成本最小,接受面最广)

答案

回到最初的问题,我们以新解释器的角度重新审视一下代码,就能彻底的理解作用域的概念啦

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
a = 1;
function a() { };
a = 2;
console.log(a); // 输出:2
}
console.log(a); // 输出:1

// 解释器解释之后的代码

var a = undefined; // 函数和变量提升至前,不给其函数实现的定义,以免块内污染块外
{
let new_a = function () { }; // 新变量名称用let进行修饰,并给出原有函数体的定义,使其函数在块内生效,在这个块中,涉及到原有变量名的都用新的变量名'new_a'
new_a = 1;
var a = new_a; // 执行到原有代码时,需要把函数用原有的名称用'var'重新修饰一下,使其块外能访问到
new_a = 2;
console.log(new_a); // 输出:2
}
console.log(a); // 输出:1

例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = 1;
{
function a() { };
a = 2;
console.log(a); // 输出:2
}
console.log(a); // 输出:function

// 解释器解释之后的代码

var a = undefined; // 变量提升
a = 1;
{
let new_a = function () { }; // 块内的方法用'let'以及新的变量名修饰, 在这个块中,涉及到原有变量名的都用新的变量名'new_a'
var a = new_a; // 执行到原有代码时,用var把原有的函数重新修饰,使其块外能访问到
new_a = 2;
console.log(new_a); // 输出:2
}
console.log(a); // 输出:function

闭包

在块级作用域出现之前,只有函数作用域和全局作用域,为了解决变量的污染,就有了闭包,何为闭包?我理解的就是立即调用一个没有名字的函数,使其变量都在该函数中

(function(arg){console.log('我是闭包,arg:', arg)})(123/*把外部变量从这里传进去*/);这样就把变量锁死在大括号中了,可以理解为对一个匿名方法的调用

也还有其他的写法如:
!function(arg){console.log('我是闭包,arg:', arg)}(123/*把外部变量从这里传进去*/);

为什么前面必须有运算符呢?可以理解为一元运算符对后面匿名变量的运算,如果把感叹号!去掉的话,执行器就不知道后面到底是什么语法了

可以理解为只要是一元运算符后面都可以接匿名变量,+或者-运算符都可以都可以

为什么大多数都用!感叹号呢,因为运算时占用的cpu和空间比较少,他就是一个取反的运算:非真即假,其次因为编写也方便

对象

谈到对象,最熟悉的莫过于this,在function中,this为谁调用我,我就是谁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Fun1(value){
this.value = value;
}
Fun1.prototype.toString = function () {
console.log(`value:${this.value}`)
};

f1 = new Fun1(1);
f1.toString()// 此时会输出1

// 如果换个对象调用输出值就变了
toString = f1.toString;
value=2 // 默认给window对象定义value属性,值为2
toString() // 输出2,因为window调用,此时window的value为2,所以输出2

JS方法中的this是可以改变的

  • test.apply(otherThis, arguments); // 参数必须传递数组
  • test.call(otherThis, ...arguments); // call只能把参数拆开, 而'...'语法就是用来拆参数的(就是用来脱衣服的)
  • test = test.bind(otherThis[,arg1[,arg2[, ...]]]); // 直接透明代理test方法,一劳永逸改变this

设计对象的结构

Ecma6之前,对象同方法,只需要new即可,至于对象的结构(包含的字段),在函数中用this,指定即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test(arg1, arg2) {
this.arg1 = arg1;
this.arg2 = arg2;
arg3 = arguments;
// 本方法名称
this.functionName = arguments.callee.name;
this.caller = arguments.callee.caller; // 本方法的调用者,为空为window,不为空则为某个函数本身,注意:caller为动态的,出栈则此属性值会变
}


var o = new test(1, 2); // 对象创建
console.log(o.arg1); // 输出:1
console.log(window.arg1); // 输出:undefined

test(3, 4); // 方法调用
console.log(window.arg2); // 输出:4
console.log(window.arg3); // 输出:[3,4]

// 一句话描述this,谁调用的我,这个this就是谁的

每个方法都有隐含的参数arguments类型为数组,该属性包含了该方法的所有参数,这个特性也就确定了js方法是没有重载的

原型链

  • prototype
    该属性对象上没有,仅为函数function所独有。
    该属性作用就是让函数所实例化的对象们都可以找到公用的属性和方法
    简称,实例的原型对象,通过实例的__proto__字段指向函数的prototype
  • __proto__
    该属性仅仅为对象所有,用来指向函数(该对象所属的function)的prototype
    每个对象找某个属性时,查找范围除了自身之外,还会从__proto__属性中查找,直至找到或到顶层为止

    由于Function是个特殊,它既是对象也是函数
    对象有 __proto__
    函数有 prototype
    所以 Function.__proto__ == Function.prototype

以上两个关系如下代码所示

1
2
3
function Fun(){}
f1 = new Fun()
f1.__proto__ == Fun.prototype

每一个函数独有prototype属性,每个对象独有__proto__属性(指向生成他函数的prototype,prototype的__proto__属性继续向上指向)

  • constructor
    构造函数,对应某个确切函数的化身,函数上的属性,方法都可在函数.prototype.constructor找到
    所以:
    函数.prototype.constructor == 函数本身
    对象实例的__.proto__.constructor == 函数本身
    对象实例的__.proto__.constructor.prototype == 函数.prototype
    一般可以通过对象获取到constructor然后获取到对应的函数,然给在给对应的函数prototype新增新的字段或方法,从而给所有实例扩展新功能

原型链继承

了解了两个最重要的对象prototype__proto__之后就知道该怎么继承了。

1
2
3
4
5
6
function Person(name, age){
this.name = name;
this.age = age;
}
san = new Person('张三', 13);
si = new Person('李四', 24);

如果让张老李四做自我介绍呢?

1
2
3
4
5
Person.prototype.toString = function () {
console.log(`你好,我叫${this.name},我${this.age}岁了`)
};
san.toString();
si.toString();

很简单吧?

如何继承Person呢?

1
2
3
4
5
6
function PersonSex(name, age, sex){
Person.call(this, name, age); // 如果不需要父类的属性删掉即可
this.sex = sex;
}
PersonSex.prototype.__proto__ = Person.prototype; // 通过原型链方式继承
new PersonSex('张三', 13, '男')

Object.defineProperty属性访问器

Ecma5就已经支持
如何给定某个属性代理get和set方法呢,如同Java那样有get和set呢?
get、set不能于value和writable同时存在(直接报错)

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
40
41
42
43
44
45
46
47
48
let Person = {}
Object.defineProperty(Person, 'name', {
value: 'jack',
writable: true, // 是否可写,默认是false
get() {
return '张三';
},// 获取 Person.name 时会触发本方法
set(arg) {

},// Person.name='张三' 时会调用
enumerable: true, // 是否可列举( for .. in | Object.keys),默认为false
configurable: true // 是否可以删除此属性,是否可以从新配置(二次调用defineProperty从新配置此属性),默认为false
});

Object.defineProperty(Person, 'age', {
value: '26',
});
// 等价于
Object.defineProperty(Person, 'age', {
value: '26',
writable: false,
enumerable: false,
configurable: false,
});

Person.gender = '男'
//等价于
Object.defineProperty(Person, 'gender', {
value: '男',
writable: true,
enumerable: true,
configurable: true
});

Person = {
_name: "李四",
get name() {
return "我叫" + this._name;
}
}
// 等价于
Person = Object.defineProperty({_name: "李四"}, 'name', {
get(){
return "我叫" + this._name;
},
enumerable: true,
configurable: true
});

proxy

ecma6才支持
如何给一个对象的所以属性增加set和get方法进行拦截呢? 用proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var obj = [];
obj = new Proxy(obj, {
get(target, prop, receiver){
let value = Reflect.get(...arguments);// 它保证将正确的 this 传递给 getter(如果有的话)
return typeof value == 'function' ? value.bind(target) : value; // 任何方法都要用target,bind一下。否则就是代理对象调用方法,this会变
},
set(target, prop, value, receiver){
return Reflect.set(...arguments);// 保证将正确的 this 传递给 setter
},
deleteProperty(target, prop) { // 拦截属性删除
return Reflect.deleteProperty(...arguments)
},
ownKeys(target) { // 拦截读取属性列表
return Reflect.ownKeys(target)
}
})
obj.push(1);
// 先读取push=function,在读取length=0(默认)
// push内部操作为:
// 1:先set key=0, value=1
// 2:在set key=length, value=1

class语法

Ecma6才支持

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
class Animals{
name;
constructor(name) {
this.name = name;
}

get name() {
return '我叫:' + this.name
}
}

class Dog extends Animals {
sex = '未知';
#age;
constructor(name, sex, age = -1) {
super(name)
this.sex = sex;
this.#age = age;
}

get str() {
return super.name + "。我的性别是" + this.sex + ",悄悄的告诉你,我" + this.#age + "岁了,不能透露给其他人哦(私有属性)";
}
}

console.log(new Dog('哈士奇','男', 18).str)
// 我叫:哈士奇。我的性别是男,悄悄的告诉你,我18岁了,不能透露给其他人哦(私有属性)
console.log(Dog.prototype.__proto__ == Animals.prototype)
// true

异步

promise

1
2
3
4
5
6
7
8
9
10
11
12
13
new Promise(function (resolve, reject) { 
// 成功
resolve('data');
// 失败
reject('errorMessage');
// 以上两者不能同时调用
}).then(function (value) {
console.log('成功了')
}).catch(function (reason) {
console.log('失败了')
}).finally(function () {
console.log('执行完啦')
})

setTimeout

1
2
3
4
5
6
7
setTimeout(function () {
console.log('1秒后执行');
}, 1000);

setTimeout(function () {
console.log('执行完这个js整体文件之后才执行');
}, 0)

setInterval

1
2
3
4
5
6
var interval = setInterval(function () {
console.log('我是周期性执行,每1秒执行一次')
},1000)

// 清空周期性执行代码
clearInterval(interval);

多线程

Worker

意义在于可以将一些耗时的数据处理操作从主线程中剥离,使主线程更加专注于页面渲染和交互

使用方式如下 var worker = new Worker('work.js'); 即可使用新线程执行另外一个js。但是有限制

  1. 执行新的js文件必须与主线程同源(不能跨域)
  2. worker全局对象与主线程不一样,无法使用dom、window对象,但是可以使用navigator和location对象
  3. 不能与主线程直接通信,必须通过事件消息的方式通讯。(消息最终都会序列化,所以也无法通过改对象里面的值使其双向通信)
  4. 不能使用alert、confirm等阻塞主线程的方法,但是可以使用XMLHttpRequest发起ajax请求
  5. 无法读取本地文件,他所加载的脚本或文件必须来自网络或者本地网络。 本地网络如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!DOCTYPE html>
    <body>
    <script id="worker" type="app/worker">
    addEventListener('message', function () {
    postMessage('some message');
    }, false);
    </script>
    </body>
    </html>
    1
    2
    3
    var blob = new Blob([document.querySelector('#worker').textContent]);
    var url = window.URL.createObjectURL(blob);
    var worker = new Worker(url);