周元-JS高级用法
# 周元-JS高级用法
# this指针/闭包/作用域
# 前端原型&原型链
// 构造函数
function Person() {}
// 构造函数的实例
const person = new Person(); // 实例化
person.name = 'zhou'
console.log(person.name); // zhou
// 构造函数
function Person() {}
// 实例的原型
Person.prototype.name = 'zhou2';
// 构造函数的实例1
const person1 = new Person();
// 构造函数的实例1
const person2 = new Person();
console.log(person1.name, person2.name); // zhou2 zhou2
Person的prototype属性指向是一个对象 <==> person1/person2创建实例的原型;
// 构造函数
function Person() {}
// 构造函数的实例
const person = new Person();
// __proto__用于获取实例的原型
console.log(person.__proto__ === Person.prototype) // true
// constructor用于返回实例的构造函数
console.log(Person.prototype.constructor === Person); // true
console.log(person.constructor === Person) // true
console.log(person.constructor === Person.prototype.constructor) // true
// getPrototypeOf用于获取实例的原型
console.log(Object.getPrototypeOf(person) === person.__proto__); // true
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
// 构造函数的原型对象的原型 等于 Object的原型
console.log(Person.prototype.__proto__ === Object.prototype);
// Object.prototype.__proto__ 的值为 null,即 Object.prototype 没有原型
console.log(Object.prototype.__proto__ === null) // true
所有的函数都有
prototype属性,这里的Person是作为构造函数来使用;每一个
JavaScript对象(除了null)都具有的一个属性,叫__proto__,这个属性会指向该对象的原型;
绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于
Person.prototype中,实际上,它是来自于Object.prototype,与其说是一个属性,不如说是一个getter/setter,当使用obj.__proto__时,可以理解成返回了Object.getPrototypeOf(obj)。
- 原型指向构造函数用
constructor,每个原型都有一个constructor属性指向关联的构造函数;
其实
person中并没有constructor属性,当不能读取到constructor属性时,会从person的原型也就是Person.prototype中读取,正好原型中有该属性。
- 原型
某一个对象,在它的原型链上的上一个节点。比如
person.__proto__就是person的原型。
- 原型链
原型上的原型,这样一个链式结构,就是原型链。
person 原型链查找:person => Person.prototype => Object.prototype => null;查找属性的时候查到 Object.prototype 就可以停止查找了
- 继承
每一个对象都会从原型‘继承’属性,继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
# 作用域
定义变量的区域。
- 静态作用域
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
函数的作用域是在函数定义时确定的。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
- 动态作用域
函数的作用域是在函数调用的时候才决定的。
// case 1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope(); // local scope
// case 2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()(); // local scope
JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。
# 执行上下文
console.log(add2(1,1)); //输出2
function add2(a,b){
return a+b;
}
console.log(add1(1,1)); //报错:add1 is not a function
var add1 = function(a,b){
return a+b;
}
用函数语句创建的函数add2,函数名称和函数体均被提前,在声明它之前就使用它。
但是使用var表达式定义函数add1,只有变量声明提前了,变量初始化代码仍然在原来的位置,没法提前执行。
# 可执行代码
executable code
- 全局代码
- 函数代码
- eval
# 执行上下文栈
ECS: execution context stack
FILO 先进后出: first in last out
- case 1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// 模拟执行上下文栈顺序:
ECStack = [
globalContext
]
1. ECStack.push(<checkscope>functionContext)
2. ECStack.push(<f>functionContext)
3. ECStack.pop();
4. ECStack.pop();
- case 2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
// 模拟执行上下文栈顺序:
ECStack = [
globalContext
]
1. ECStack.push(<checkscope> functionContext);
2. ECStack.pop();
3. ECStack.push(<f> functionContext);
4. ECStack.pop();
# 变量对象
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。 对于每个执行上下文,都有三个重要属性:
- 变量对象 varible object(VO)
- 作用域链 scope chain
- this
变量对象:上下文中定义的变量和函数的声明
global、function
# 全局上下文
this => 全局对象
全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。
JS 全局上下文的VO => 全局对象
对JS而言,全局上下文中的变量对象就是全局对象。
console.log(this); // window对象
console.log(this instanceof Object); // true
console.log(Math.random());
console.log(this.Math.random());
# 函数上下文
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
活动对象:activiation object(AO)
活动对象和变量对象其实是一个东西,只是变量对象不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
# 执行代码
执行上下文的代码会分成两个阶段进行处理:分析和执行
- 分析:进入执行上下文
- 执行:代码执行
- 进入执行上下文
当进入执行上下文时,这时候还没有执行代码,变量对象会包括:
- 函数的所有形参
- 函数声明
- 变量声明
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
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 对象;
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
- 在代码执行阶段,会再次修改变量对象的属性值;
- 例1:
function foo() {
console.log(a);
a = 1;
}
foo(); // Uncaught ReferenceError: a is not defined
// 函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。
// 第一段执行 console 的时候, AO 的值是:
AO = {
arguments: {
length: 0
}
}
// 没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。
function bar() {
a = 1;
console.log(a);
}
bar(); // 1
// 这里执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印
- 例2:
console.log(foo); // 会打印函数
function foo(){
console.log("foo");
}
var foo = 1;
// 这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
var foo = 1;
console.log(foo); // 1
function foo(){
console.log("foo");
}
console.log(foo); // 1
先处理函数声明,再处理函数赋值,然后执行,看当前位置foo是否有被重新赋值,有则打印1,无则打印函数
# 作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
- 函数创建
函数的作用域在函数定义的时候就决定了。
函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
function foo() {
function bar() {
...
}
}
// 函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
- 函数激活
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
这时候执行上下文的作用域链,我们命名为 Scope:
Scope = [AO].concat([[Scope]]);
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
// 执行过程如下:
// 1. checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
// 2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
// 3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
// 4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
// 5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
// 6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
// 7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
# this
ECMAScript 类型
- 语言类型:String / bool / Null
- 规范类型:Reference, Property Descriptor, Environment Record, ...
用来描述语言底层行为逻辑~
# Reference 类型
Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。
Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。
Reference 的构成,由三个组成部分,分别是:
base value:base value 就是属性所在的对象referenced name:属性的名称strict reference;
var foo = 1;
// 对应的Reference是:
var fooReference = {
base: EnvironmentRecord,
referenced name: 'foo',
strict reference: false
};
// GetBase
GetBase(fooReference) // base
// GetValue 返回对象属性真正的值
GetValue(fooReference) // 1;
var foo = {
bar: function () {
return this;
}
};
foo.bar(); // foo
// bar对应的Reference是:
var BarReference = {
base: foo,
referenced name: 'bar',
strict reference: false
};
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1
# 闭包
闭包:能够访问自由变量的函数。
自由变量:能在函数中使用,但既不是函数参数也不是函数的局部变量的变量。
函数 + 函数里能够访问非自身的变量
var a = 1;
function foo() {
console.log(a);
}
foo();
foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。
在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。
ECMAScript中,闭包指的是:
- 从理论角度:所有的函数。
因为它们都在创建的时候就将上层上下文的数据保存起来了。函数中访问全局变量就相当于是在访问自由变量;
- 从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回);
- 在代码中引用了自由变量;
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
console.log(this); // Window
return scope;
}
return f;
}
var foo = checkscope();
foo(); // local scope
// 全局上下文
VO = {
scope: 'global scope',
checkscope: reference checkscope function
}
作用域链:[VO]
this: window
// checkscope上下文
AO = {
arguments: {
length: 0
},
scope: 'local scope',
f: reference f() {}
}
作用域链:[AO, VO]
// this
ECStack = [
<f>functionContext,
<checkscope>functionContext,
globalContext
]
{/* f上下文 */}
AO = {
arguments: {
length: 0
},
}
作用域链:[<f>AO,<checkscope>AO, VO]
ECStack = [
<f>functionContext,
globalContext
]
Q: 当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
# 传递参数
- 按值传递
ECMAScript中所有函数的参数都是按值传递的。
把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。
var value = 1;
function foo(v) {
v = 2;
console.log(v); //2
}
foo(value);
console.log(value) // 1
- 按引用传递
所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。
var obj = {
value: 1
};
function foo(o) {
o.value = 2;
console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
又叫共享传递,在传递对象的时候,传递的是地址索引。
参数如果是基本类型是按值传递,如果是引用类型按共享传递。
Q:为什么《JavaScript高级程序设计》都说了 ECMAScript 中所有函数的参数都是按值传递的,那为什么能按"引用传递"成功呢?
参数如果是基本类型是按值传递,如果是引用类型按共享传递。但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。
函数传递参数 ,传递的是参数的拷贝:
- 指针拷贝,拷贝的是地址索引;
- 常规类型拷贝,拷贝的是值 ;
javascript中数据类型分为基本类型与引用类型:
- 基本类型值存储于栈内存中,传递的就是当前值,修改不会影响原有变量的值;
- 引用类型值其实也存于栈内存中,只是它的值是指向堆内存当中实际值的一个地址;索引引用传递传的值是栈内存当中的引用地址,当改变时,改变了堆内存当中的实际值;
# call 和 apply
# call实现
在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
call改变了this指向。
模拟的步骤可以分为:
- 将函数设为对象的属性;
- 执行该函数;
- 删除该函数;
// 第一步
// fn 是对象的属性名,反正最后也要删除它,所以起什么都可以。
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn
- 完整版:
Function.prototype.call2 = function(context) {
var context = context || window; // 兼容this参数传 null的情况
context.fn = this; // 即:foo.fn = bar;
let arg = [...arguments].slice(1) // 指定参数
let result = context.fn(...arg)
delete context.fn
return result // 实现返回值
}
// 测试一下
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'kevin', 18);
// kevin
// 18
// 1
Symbol写法:
Function.prototype.call2 = function(context, ...args) {
// 判断是否是undefined和null
if (typeof context === 'undefined' || context === null) {
context = window
}
let fnSymbol = Symbol()
context[fnSymbol] = this
let fn = context[fnSymbol](...args)
delete context[fnSymbol]
return fn
}
# apply实现
apply 的实现跟 call 类似,只是入参不一样,apply为数组
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
}
else {
result = context.fn(...arr)
}
delete context.fn
return result;
}
// Symbol写法
Function.prototype.apply2 = function(context, args) {
// 判断是否是undefined和null
if (typeof context === 'undefined' || context === null) {
context = window
}
let fnSymbol = Symbol()
context[fnSymbol] = this
let fn = context[fnSymbol](...args)
delete context[fnSymbol]
return fn
}
# bind的实现
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
bind 函数的两个特点:
- 返回一个函数;
- 可以传入参数;
- 版本1
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}
/**
* 调用
*/
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18
// new调用
var obj = new bindFoo('18');
// undefined (绑定的 this 失效, 已经指向obj了)
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
- 版本2,兼容使用new时this失效问题:
// 第三版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fBound.prototype = this.prototype;
return fBound;
}
- this: 新创建出来的obj实例,这个实例是通过 new fBound 创建
obj instanceof fBound
# 手写new
因为 new 是关键字,所以无法像 bind 函数一样直接覆盖,所以我们写一个函数,命名为 objectFactory,来模拟 new 的效果。用的时候是这样的:
function Person () {
……
}
// 使用 new
var person = new Person(……);
// 使用 objectFactory
var person = objectFactory(Person, ……)
- 实现:
function Person (name, age) {
this.name = name;
this.age = age;
this.habit = 'Games';
}
Person.prototype.strength = 60;
Person.prototype.sayYourName = function () {
console.log('I am ' + this.name);
}
function objectFactory() {
var obj = new Object(),
Constructor = [].shift.call(arguments); // 获取传入的第一个参数作为构造函数
obj.__proto__ = Constructor.prototype; // 获取原型对象
Constructor.apply(obj, arguments); // 传入其他参数
// return obj; // 返回obj
// 优化:
/**
* 构造函数返回了一个对象,在实例 person 中只能访问返回的对象中的属性;
* 返回基本数据类型,则不对返回值进行处理
*/
return typeof ret === 'object' ? ret : obj;
};
var person = objectFactory(Person, 'Kevin', '18')
console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60
person.sayYourName(); // I am Kevin
# 类数组对象与arguments
# 类数组对象
拥有一个 length 属性和若干索引属性的对象
var array = ['name', 'age', 'sex'];
var arrayLike = {
0: 'name',
1: 'age',
2: 'sex',
length: 3
}
// 读写
console.log(array[0]); // name
console.log(arrayLike[0]); // name
array[0] = 'new name';
arrayLike[0] = 'new name';
// 长度
console.log(array.length); // 3
console.log(arrayLike.length); // 3
// 遍历
for(var i = 0, len = array.length; i < len; i++) {
……
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
……
}
// 但是调用原生的数组方法会报错,如push:
// arrayLike.push is not a function
- 调用数组方法
只能通过
Function.call间接调用~
var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
Array.prototype.join.call(arrayLike, '&'); // name&age&sex
Array.prototype.map.call(arrayLike, function(item){
return item.toUpperCase();
});
// ["NAME", "AGE", "SEX"]
/**
* 类数组转数组
*/
// 1.slice
Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"]
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"]
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"]
// 4. apply
Array.prototype.concat.apply([], arrayLike)
# Arguments对象
Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。
function foo(b, c, d){
console.log(arguments) // 除了类数组的索引属性和length属性之外,还有一个callee属性
console.log("实参的长度为:" + arguments.length) // 实参的长度为:1
}
console.log("形参的长度为:" + foo.length) // 形参的长度为:3
foo(1)
/**
* callee
*/
// 闭包经典面试题使用 callee 的解决方法:
var data = [];
for (var i = 0; i < 3; i++) {
(data[i] = function () {
console.log(arguments.callee.i) // Arguments 对象的 callee 属性,通过它可以调用函数自身。
}).i = i;
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
- 传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享