本文主要涉及两方面的内容:
- JavaScript的公有、私有、静态属性和方法
- 运算符的优先级
- 返回值
- this指向
文章目录
开胃菜
输出什么?
function A(x) {
this.x = x;
}
A.prototype.x = 1;
function B(x) {
this.x = x;
}
B.prototype = new A();
var a = new A(2),
b = new B(3);
delete b.x;
console.log(a.x, b.x)
答案是 2 undefined
(❗️提示:下边说的实例化、公有属性,你听不懂没关系,这个文章就是解释这些的。但是原型啊什么的你听不懂,你就按照我的提示去补充知识吧。👻)
为什么a.x输出2
我们先看一下a本身的构造。
a是A的实例,那么a的隐式原型就指向A的显式原型,也就是说A的prototype
和a的__proto__
相等。输出一下,确实是相同的。(这里看不懂就去补原型的知识)
那这个题的A就是这样的:
A.prototype.x = 1;
是给A的原型上添加一个公有属性。this.x = x;
是A函数体里边的公有属性。
当函数体本身有某个方法或者属性的时候,就会把原型链里边的遮盖掉。如果函数体中没有某个方法或者属性,就回去原型链中找。
题目里实例化的对象直接使用.
运算符,是直接访问A函数体里边的公有属性,而A函数体中有个x了,所以不会用到原型链里的x。想要访问到原型链里的x只能去原型链里找。
function A(x) {
this.x = x;
}
A.prototype.x = 1
var a = new A(2)
console.log(a.x)
console.log(a.__proto__.x)
函数体中没有x的时候,就直接使用原型链里边的了。
function A(x) {
// this.x = x;
}
A.prototype.x = 1
var a = new A(2)
console.log(a.x)
console.log(a.__proto__.x)
为什么b.x输出undefined
先分析一下B的代码:
function B(x) {
this.x = x;
}
B.prototype = new A();
创建一个函数B,B.prototype = new A();
这句话就是B.prototype
是A的实例,也就是说B.prototype
的隐式原型就是A的显式原型。
那对于var b = new B(3);
现在的逻辑就是实例b用.
运算符访问x,就是访问b中的共有属性x:
B
函数体中有x,那就访问x
B
函数体中没有x,那就访问B.prototype.x
B.prototype
中没有x,那就访问B.prototype__proto__.x
捋清楚访问的逻辑了吧,那现在我们就来看一下输出什么:
function A(x) {
this.x = x;
}
A.prototype.x = 1;
function B(x) {
this.x = x;
}
B.prototype = new A();
var b = new B(3);
delete b.x;
console.log(b.x)
因为B函数体中有x,实例化b = new B(3)
之后,b是B的实例,此时B函数体中的x = 3 。
b.x指的是B函数体中的x。
delete b.x;
将函数体中的x删除了。此时b.x指的就是b原型链上的b了,也就是先看看B.prototype
中有没有x。
那我们看看b的构造:b__proto__
中也就是B.prototype
中有个x!并且还是undefined,找到x了,直接输出undefined即可。
undefined哪里来的?
B.prototype = new A();
B的显式原型是A是实例对象,A函数体中有x,B.prototype
实例化的时候没有携带参数,也就是没给A的x赋值,所以输出undefined。
不信你就试一下,给B.prototype = new A();
加上个值。
进入正题
刚才我一直在说共有属性公有属性,那还有私有的?
其实不仅有私有的,还有静态的嗷。
function User(id) {
this.id = id; //公有属性
var id = id; //私有属性
function getId() { //私有方法
console.log(this.id)
}
}
User.prototype.getId = function() { //公有方法
console.log(this.id)
}
User.id = 'Sian静态'; //静态属性
User.getId = function() { //静态方法
console.log(this.id)
}
var sian = new User('SianOvO'); //实例化
- 调用公有方法、公有属性,需要先实例化对象,也就是用new操作符,公有方法不能调用私有方法和静态方法的
console.log(sian.id) //输出SianOvO sian.getId() //输出SianOvO
- 调用静态方法、静态属性,无需实例化
console.log(User.id) //输出Sian静态 User.getId() //输出Sian静态
- 私有方法、私有属性外部是不可以访问的
看懂了就做个题
看到刚才那里你可能觉得自己又懂了,那做个题吧。👏
请写出以下输出结果:
function Foo() {
getName = function () { console.log(1); };
return this;
}
Foo.getName = function () { console.log(2);};
Foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName() { console.log(5);}
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
直接告诉你答案:2 4 1 1 2 3 3
这个题综合了很多知识,看了答案不懂也没事,往下看解析。
整理代码
拿到这段代码之后,要进行预处理:变量提升(hoisting),先function声明的函数,再var声明的变量,所以提升以后代码逻辑如下:
var getName = undefined
function Foo() {
getName = function () { console.log(1) }
return this
}
function getName() { console.log(5) }
Foo.getName = function () { console.log(2) }
Foo.prototype.getName = function () { console.log(3) }
getName = function () { console.log(4) }
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
看一下上边的5,6,10行代码做了什么:
- var声明一个变量getName
- function声明一个函数getName
- 给getName赋值,把原来function声明的函数覆盖掉
所以覆盖之后代码逻辑如下:
var getName = undefined
function Foo() {
getName = function () { console.log(1) }
return this
}
Foo.getName = function () { console.log(2) }
Foo.prototype.getName = function () { console.log(3) }
getName = function () { console.log(4) }
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
思路分析
整理完代码逻辑可以做题啦
第1问Foo.getName();
没实例化,直接使用构造函数 + .
运算符,这是访问静态方法。
执行过程:
- 执行Foo的静态方法:
Foo.getName = function () { console.log(2) }
- 所以输出2
第2问getName();
执行过程:
- 调用全局的getName方法
var getName = function () { console.log(4) }
。 - 所以输出4
第3问Foo().getName();
先看一下Foo()
函数:
getName = function () { console.log(1) }
的作用就是给getName赋值一个函数。Foo()
函数体中没有声明getName变量,因此往上一层作用域中找,找到了window,于是更改全局作用域(window)中的getName方法。- 函数直接调用,this指向window。
return this
就是返回window。
执行过程:
- 执行
Foo()
修改getName方法并将this指向window - 调用window的getName方法
function () { console.log(4) }
- 所以输出1
第4问getName();
跟上一题一样,就是window.getName();
执行过程:
- 调用window的getName方法
function () { console.log(4) }
- 所以输出1
第5问new Foo.getName();
一头雾水?这里是考察的是JS的运算符优先级问题。放个优先级表格(完整版戳👉MDN web docs 运算符优先级)
本题中涉及到的运算符:
- 19
- 成员访问
… . …
- new (带参数)
new … ( … )
- 函数调用
… ( … )
- 成员访问
- 18
- new(无参数)
new …
- new(无参数)
注意new Foo.getName();
这个括号是函数的括号啊,跟表格里权重20的括号不一样,权重20的括号是
(
1
+
3
)
/
2
(1+3)/2
(1+3)/2运算中的括号啊!!!
再看new Foo.getName();
该怎么解析:
解析到new
之后,继续往后解析,看看需要new什么东西,也就是new Foo
。但是Foo
后边却有个.
运算符。此时就是没有new无参数列表
(18)和.
(19)比较,.
的优先级高,所以先执行Foo.getName
。那执行顺序就变为下图:
此时(Foo.getName)
后边有个()
,此时new无参数
(18)就变成了new有参数
(19),执行带参数列表的new。
再次提醒,这个()
括号是调用函数时的括号,跟(Foo.getName)
的运算符括号不同。
执行过程:
Foo.getName
就是Foo.getName = function () { console.log(2) }
new Foo.getName();
就是把function () { console.log(2) }
当作构造函数,创建其实例对象。- 因此输出2
Q:为什么有输出啊?我也没调用函数吧?
A:如果你有这个疑问,建议看一哈JavaScript中 构造函数的new都做了什么。new的第三步就是执行构造函数中的代码,因此不需要你调用,new 的过程中它自己就执行啦。
第6问new Foo().getName();
第五问搞明白之后。剩下的就好懂多了。
new ()
和.
的优先级是一样的,从左往右执行,所以先执行new Foo()
,再次修改全局中的getName,然后返回一个this,这个this是指向new Foo()
这个实例化对象的。然后执行实例化对象的getName方法。
注意!这个看似跟第3问一样,其实不一样,第三问中是直接调用Foo(),但是本问中进行了new实例化操作。
执行过程:
- 创建Foo的实例对象,this指向实例对象
- 调用实例对象的getName方法(实例对象能调用构造函数的公有方法和属性)
- 先看函数体中有没有getName方法,函数体中没有
getName
方法,需要去原型链中寻找。
虽然我们看代码可以看到函数体有个getName ,但是那个getName是私有方法!对于代码执行来说外部是访问不到的。 - 实例的原型链有找到了,Foo的prototype中有getName方法
Foo.prototype.getName = function () { console.log(3) }
,因此执行getName方法
- 先看函数体中有没有getName方法,函数体中没有
- 所以输出3。
第7问new new Foo().getName();
遇到new
,继续向右解析,又遇到new
,继续向右解析,遇到()
和.
,new()
优先级等于.
,执行new()
。下图左边是一个new,没参数列表,右边是带参数列表,执行右边的new。
执行过程:
let 实例 = new Foo()
指针指向实例let x = 实例.getName
x 就是ƒ () { console.log(3) }
new x()
就是将x实例化,实例化的过程中自动调用ƒ () { console.log(3) }
- 所以输出3
再来个题自己测验一下
function Foo() {
this.getName = function() {
console.log(1)
return { getName: getName }
}
getName = function() { console.log(2) }
return this
}
Foo.getName = function() { console.log(3) }
Foo.prototype.getName = function() { console.log(4) }
var getName = function() { console.log(5) }
function getName() { console.log(6) }
Foo.getName()
getName()
Foo().getName()
getName()
new Foo.getName()
new Foo().getName()
new Foo().getName().getName()
new new Foo().getName()
不详细将了,大致说一下思路。
预处理之后的代码:
var getName = undefined
function Foo() {
this.getName = function() {
console.log(1)
return { getName: getName }
}
getName = function() { console.log(2) }
return this
}
Foo.getName = function() { console.log(3) }
Foo.prototype.getName = function() { console.log(4) }
getName = function() { console.log(5) }
Foo.getName()
调用Foo的静态方法,输出3getName()
调用全局变量的getName,输出5Foo().getName()
直接调用Foo函数,this指向window,this.getName = function(){...}
修改一次window中的getName,getName = function(){...}
又修改一次window中的getName,输出2
getName()
window中的getName,输出2new Foo.getName()
–>new (Foo.getName)()
,输出3new Foo().getName()
–>(new Foo()).getName()
,this.getName = function() {...}
是公有方法,输出1。
如果没有this.getName = function() {...}
这一句,就是输出4new Foo().getName().getName()
–>((new Foo()).getName()).getName()
到这里((new Foo()).getName())
和第6问一模一样,但是后边又加了个.getName() ,这个.getName()
是谁的?- 第6问调用的公有方法
this.getName = function() {...}
,里边还有return { getName }
,返回了一个对象,并且给对象的getName属性赋值一个getName方法 - 但是
this.getName = function() {...}
里边没getName方法,往上一层找,找到Foo里边的getName = function() { console.log(2) }
,输出2 - 如果你把
getName = function() { console.log(2) }
这句注释掉。那就是this.getName里边没getName方法,往上一层找Foo里边也没有,继续往外找,在全局作用域中找到了,输出5。
- 第6问调用的公有方法
new new Foo().getName()
–>new ((new Foo()).getName())
((new Foo()).getName())
就是把this.getName = function() {...}
当构造函数实例化,new的过程中执行代码,输出1
上边提到了this,那就再补充几个小题目
function Car() {
this.make = "Lamborghini"
return { make: "Maserati" }
}
const myCar = new Car()
console.log(myCar.make)
输出Maserati
myCar是Car的实例,但是Car有个返回值是返回到一个对象,所以调用myCar就是调用这个对象,输出的就是该对象的name “Maserati”
var status = "😎"
setTimeout(() => {
const status = "😍"
const data = {
status: "🥑",
getStatus() {
return this.status
}
}
console.log(data.getStatus())
console.log(data.getStatus.call(this))
}, 0)
输出🥑和😎
在data中调用getStatus()方法,getStatus()的this是指向data的,因此第一个输出🥑。第二个使用call方法,将其绑定给this,这个this是指向全局的。
我尽力了,讲的很繁琐,但是应该讲的很明白了