JavaScript的标准是 ECMAScript 。截至 2012 年,所有浏览器都完整的支持ECMAScript 5.1,旧版本的浏览器至少支持 ECMAScript 3 标准。2015年6月17日,ECMA国际组织发布了 ECMAScript 的第六版,该版本正式名称为 ECMAScript 2015,但通常被称为 ECMAScript 6 或者ES2015。 到现在更新到了 ECMAScript 2022(ES13)。ES6 中新特性最多,后面的版本则每次进行少量更新。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域,函数的作用域在函数定义的时候就决定了。而在动态作用域中,函数的作用域是在函数调用的时候才决定的。
《JavaScript权威指南》中的例子
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope(); // => local scope
由于JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。这里 f 函数创建在 checkscope 函数内部,所以当 f 函数找不到 scope
时,会向当前作用域,即在 checkscope 函数内部查找 scope
《JavaScript权威指南》的回答就是:
JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。
另外一个简单的例子
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // => 1
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value = 1,所以结果会打印 1。即函数的作用域在函数定义的时候就决定了。
tips: 参考 https://juejin.cn/column/7035531575974592520
可执行代码
JS中分为3种:全局代码、函数代码、eval 代码。
当执行到一个函数的时候,就会创建执行上下文(execution context)
执行上下文栈
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文我们定义执行上下文栈是一个数组:ECStack = [];
JS 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空。
我们来分析下面代码:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
// fun1()
ECStack.push(<fun1> functionContext);
// fun2
ECStack.push(<fun2> functionContext);
// fun3
ECStack.push(<fun3> functionContext);
// fun3执行完毕
ECStack.pop();
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
对于每个执行上下文,都有三个重要属性:
变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。变量对象中又可分为全局上下文下的变量对象
和函数上下文下的变量对象
。
1.在客户端 JavaScript 中,全局对象就是 Window 对象,可以通过 this 引用。
console.log(this); // window
2.全局对象是由 Object 构造函数实例化的一个对象。
console.log(this instanceof Object);
3.作为全局变量的宿主。
var a = 1;
console.log(this.a);
4.客户端 JavaScript 中,全局对象有 window 属性指向自身。
var a = 1;
console.log(window.a);
this.window.b = 2;
console.log(this.b);
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JS 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object ,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
通过var声明的变量才被放入AO中,也会出现变量提升
执行过程
当进入执行上下文时,这时候还没有执行代码,变量对象会包括:
undefined
如果变量对象已经存在相同名称的属性,则完全替换这个属性
undefined
)组成一个变量对象的属性被创建;举个例子:
function foo(a) {
console.log(b) //undefined
var b = 2;
function c() {} // 函数
var d = function() {}; // 变量
var c = 1 // 不影响c
b = 3;
}
foo(1);
在进入执行上下文后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值,当代码执行完后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
全局上下文的变量对象初始化是全局对象
函数上下文的变量对象初始化只包括 Arguments 对象
在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
在代码执行阶段,会再次修改变量对象的属性值
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
函数有一个内部属性 [[scope]],当函数创建
的时候,就会保存所有父变量对象到其中。当函数激活
时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。这时候执行上下文的作用域链,我们命名为 Scope。
函数执行上下文中作用域链和变量对象的创建过程:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
checkscope.[[scope]] = [
globalContext.VO
];
ECStack = [
checkscopeContext,
globalContext
];
checkscopeContext = {
Scope: checkscope.[[scope]],
}
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
ECStack = [
globalContext
];
在源代码中当定义(书写)一个函数的时候(并未调用),js引擎也能根据函数书写的位置,函数嵌套的位置,生成一个 [[scope]],作为该函数的属性存在(这个属性属于函数的)。
然后进入函数执行阶段,生成执行上下文,执行上下文可以宏观的看成一个对象,(包含vo,scope,this),此时,执行上下文里的scope
(作用域链)和之前属于函数的那个 [[scope]] 不是同一个,执行上下文里的scope,是在之前函数的[[scope]]的基础上,又新增一个当前的AO对象构成的。
函数定义时候的[[scope]]和函数执行时候的scope,前者作为函数的属性,后者作为函数执行上下文的属性。
JS执行上下文全过程见 https://blog.csdn.net/by6671715/article/details/105241528
var obj = {
a: 1,
b: function(){console.log(this);}
}
obj.b();
// 指向objvar b = obj.b; b();
// 指向全局windowvar b = new Fun();
// this指向当前实例对象obj.b.apply(object, []);
// this指向当前的objectbind,apply,call
function fun() {
console.log(this.name, arguments);
}
let obj = {name: 'alex'}
let b = fun.bind(obj, 'others')
b('another')
function fun() {
console.log(this.name, arguments);
}
let obj = {name: 'alex'}
fun.apply(obj, ['other'])
function fun() {
console.log(this.name, arguments)
}
let obj = { name: 'alex' }
fun.call(obj, 'other')
闭包(closure)是一个函数以及其捆绑的周边环境状态,让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
经典例子
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp(); // 将全部提示 'Your age (you must be over 16)'
赋值给 onfocus 的是闭包,这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量 item。这是因为变量 item 使用 var 进行声明,由于变量提升,所以具有函数作用域。当 onfocus 的回调执行时,item.help 的值被决定。由于循环在事件触发之前早已执行完毕,变量对象 item(被三个闭包所共享)已经指向了 helpText 的最后一项。
解决办法1:
// 嵌套闭包
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
document.getElementById(item.id).onfocus = makeHelpCallback(item.help)
解决办法2:
// 立即执行函数(匿名闭包)
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})();
解决办法3:
// 使用 let 避免 var 变量提升
for (let i = 0; i < helpText.length; i++) {
//...
}
__proto__
每个JavaScript对象(除了 null )都具有的一个属性,这个属性会指向该对象的原型。
function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = new Person('Alex', 16);
// Person 类型实例的原型,默认也是一个空对象
console.log(person.__proto__); // => Person {}
//es6 新方法
console.log({}.__proto__ === Object.getPrototypeOf({})); // true
可以直接修改对象的原型,不过被设置的值的类型只能是对象或者 null,其它类型不起作用:
const obj = { name: 'Alex' };
console.log(obj.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
obj.__proto__ = 666;
// 非对象和 null 不生效
console.log(obj.__proto__ === Object.prototype); // true
obj.__proto__ = null;
// 设置为 null 返回 undefined
console.log(obj.__proto__=== Object.prototype); // false
// 设置原型为对象
obj.__proto__ = { constructor: 'Function Student' };
console.log(obj.__proto__); // { constructor: 'Function Student' }
如果被设置的值是不可扩展的,将抛出 TypeError:
const frozenObj = Object.freeze({});
// Object.isExtensible(obj) 可以判断 obj 是不是可扩展的
console.log(Object.isExtensible(frozenObj)); // => false
frozenObj.__proto__ = null; // => TypeError: #
注:原型上的属性都是不可枚举的:Object.keys(obj)// [ ]
其实 __proto__ 是个访问器属性(getter 和 setter 都有),通过 __proto__ 访问器我们可以访问对象的 [[Prototype]], 也就是原型。
Object.prototype = {
get __proto__() {
return this['[[prototype]]'];
},
set __proto__(newPrototype) {
if (!Object.isExtensible(newPrototype)) throw new TypeError(`${newPrototype} is not extensible`);
if (newPrototype === null) {
this['[[prototype]]'] = undefined;
return;
}
const isObject = typeof newPrototype === 'object' || typeof newPrototype === 'function';
if (isObject) {
this['[[prototype]]'] = newPrototype;
}
}
};
prototype
function Person() {
}
Person.prototype.name = 'Alex';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Alex
console.log(person2.name) // Alex
这里,Person 就是一个构造函数,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。
可以将原型理解为: 每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
每个函数都有一个 prototype属性,默是一个空的纯对象,所有由构造器构造的实例的原型都是指向它 ,比如:
// 实例的原型即 person1.__proto__
console.log(person1.__proto__ === Person.prototype); // => true
console.log(person2.__proto__ === Person.prototype); // => true
constructor
在 JavaScript 中,函数都可以用作构造器。构造器我们也可以称之为类,我们可以通过 new 构造器来构造一个实例。如上面的person1,person2。
每个原型都有一个 constructor 属性指向关联的构造函数。
function Person() {
}
console.log(Person === Person.prototype.constructor); // true
__proto__ ,prototype,constructor,Person函数,实例 person 和原型对象 [[prototype]] 之间的关系:
__proto__ 存在于所有的对象上,prototype 存在于所有的函数上,他俩的关系就是:函数的 prototype 是所有使用 new 这个函数构造的实例的 __proto__。函数也是对象,所以函数同时有 __proto__ 和prototype。
实例与原型
当在一个对象 obj 上访问某个属性时,如果不存在于 obj,那么便会去对象的原型也就是 obj.__proto__ 上去找这个属性。如果有则返回这个属性,没有则去对象 obj 的原型的原型也就是 obj.__proto__.__proto__去找,重复以上步骤。一直到访问纯对象的原型的原型{}.__proto__.__proto__,也就是 null,直接返回 undefined。
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
原型链
由上可知Object.prototype 的原型为null,即Object.prototype.__proto__ 的值为 null ,也可以说 Object.prototype 没有原型。所以查找属性的时候查到 Object.prototype 就可以停止查找了。
各个原型之间构成的链,我们称之为原型链。
函数(构造器) Person 的原型链:
原型的用途
在使用构造器定义一个类型的时候,一般会将类的方法定义在原型上
function Cat(workingYears) {
this.category = category;
}
// 不能使用箭头函数,箭头函数的 this 在声明的时候就根据上下文确定了
Cat.prototype.voice = function() {
console.log(`${this.category} miaomiao叫`);
};
const cat = new Cat('大橘');
cat.voice(); // 大橘 miaomiao叫
通过这种方式,所有的实例都可以访问到这个方法,并且这个方法只需要占用一份内存,节省内存,this 的指向还能正确指向类的实例。不过这种方式定义的方法都是不可枚举的,毕竟不是自身的属性:
const obj = {
func() {}
};
console.log(Object.keys(obj)); // => [ 'func' ]
function Func() {}
Func.prototype.func = function() {};
// 空数组,说明 func 不可枚举
console.log(Object.keys(new Func())); // => []
ES6 class
ES6 class 就是构造器的语法糖。ES6 的 class 就是构造器,class 上的方法定义在构造器的 prototype 上。
class Person {
constructor(name) {
// 构造函数
this.name = name
}
}
// extends 继承
class Male extends Person {}
let male = new Male('Alex')
console.log(male.__proto__.__proto__ === Person.prototype) // true
console.log(Male.__proto__ === Person) // true
原型继承
对于类 A 和类 B,如果满足 A.prototype.__proto__ === B.prototype,那么 A 原型继承 B
实现方法1
function A() {}
function B() {}
A.prototype = new B();
console.log(A.prototype.__proto__ === B.prototype); // true
实现方法2 (避免 B 的实例属性污染 A 的原型)
function A(p) {
this.p = p;
}
function B() {}
// 空函数
function Empty() {}
Empty.prototype = B.prototype;
A.prototype = new Empty();
// 修正 constructor 指向
A.prototype.constructor = A;
// 满足原型继承的定义
console.log(A.prototype.__proto__ === B.prototype); // => true
const a = new A('p');
console.log(a instanceof A); // => true
const b = new B();
console.log(b instanceof B); // => true
// a 也是 B 的实例
console.log(a instanceof B); // => true
console.log(a.__proto__.__proto__ === B.prototype); // => true
实现方法3 (利用 Object.create):
function _inheritsLoose(subClass, superClass) {
// Object.create(prototype) 返回一个以 prototype 为原型的对象
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}