整理了一下学习js时遇到的问题,一些与其他语言相同的共性这里就省略了。

Javascrpit笔记

变量

变量提升

1
2
console.log(a); //undefined
var a = 10;

变量提升即变量可以引用稍后声明的变量而不会引发异常。提升即将声明提到其作用域的顶部,但是初始化却不会提升,所以赋值给a的操作无法是调用之前进行。

1
2
3
4
5
6
7
8
9
var i=0;
function funcA() {
console.log(i); //undefined
var i = 1;
}
function funcB() {
console.log(i); //0
}
func();

变量提升与变量自身的作用域有关,上述funcA因为函数内部作用域的变量i函数提升了,无法读出函数外部全局变量i,而funcB内部不存在同名变量,因此可以访问全局变量i

函数提升

1
2
3
4
a();
function a() {}
f(); // TypeError
f = function b() {};

对于函数,只有函数声明会被提升到顶部,而不包括函数表达式。

1
2
3
4
5
6
console.log(a); // function a
var a = 10; // var a被忽视,执行 a=10的赋值操作
function a() {
console.log('aFunc');
}
console.log(a); // 10

因为函数提升比变量提升的优先级高,所以第一个log输出函数a。

let关键字

1
2
console.log(a); //抛出ReferenceError
let a = 10;

let用来声明变量,不过与var不同,let声明的变量不能变量提升。

1
2
3
4
5
6
if true {
var a = 10;
let b = 20;
}
console.log(a); //能够访问
//console.log(b); 不能访问

let声明的变量作用范围也不能超出代码块。

变量的解析顺序

1
2
3
4
5
6
7
var x = 20;
function func(){
console.log(x); // 变量提升 undefined
var x=20;
console.log(x); // 20
}
func();

局部变量优先级大于外部(全局)变量

1
2
3
4
5
6
function func(x){
console.log(x); // 30
var x = 20;
console.log(x); // 20
}
func(30);

虽然内部变量提升了,但是由于形参优先级更高,并且内部变量没有在第一个log前赋值,所以此时第一个log输出形参

1
2
3
4
5
function func(x){
console.log(x); // 输出x函数
function x(){}
}
func(20);

由于函数提升了,函数的优先级比形参的高,所以形参x被函数x覆盖了。

1
2
3
4
5
function func(x,f = ()=> x){
var x = 30;
console.log(f()); // 20
}
func(20);

参数列表中的读取的值为同个参数列表其他值

连等赋值问题

1
2
3
4
5
var a = {n:1};
var b = a;
a.x = a = {n:2}; // 与a = a.x = {n:2}结果是一致的
alert(a.x); // --> undefined
alert(b.x); // --> {n:2}

.的优先级大于=号。

  1. 声明a对象中的x属性,用于赋值,此时b指向a,同时拥有未赋值的x属性
  2. 对a对象赋值,此时变量名a改变指向到对象{n:2}
  3. 对步骤1中x属性,b指向的对象(a原指对象)的x属性赋值

面向对象

构造函数创建对象

js中没有类,但有构造函数可以创建新实例。

1
2
3
4
5
6
7
8
9
10
function Person(name,age) {
this.name = name;
this.age = age;
this.func = function() {
console.log(this.name);
}
}
var person1 = new Person('xxx',10); // 使用new创建实例
console.log(person1); // 输出object

Object()构造函数创建对象

1
2
3
4
var person1 = new Object({
name : 'xxx',
age : 10
});

继承

Javascript的继承通过原型实现,当访问对象没定义的属性和方法时,会通过 原型链 一层一层的访问。每个对象的指针指向原型链的上一层,这个指针为proto即原型对象,一般对象的proto都指向Js的内置对象Object。

1
2
3
4
5
console.log(instance.__proto__);
var one = {};
var two = new Object();
one.__proto__ === Object.prototype; // true
two.__proto__ === Object.prototype; // true

对象的proto属性的值就是它所对应的原型对象。实例才有proto,而Object.prototype指向原型对象。

prototype属性

对象定义的prototype属性是继承成员被定义的地方。不像每个对象都有proto属性来标识自己所继承的原型,只有函数才有prototype属性,当创建函数是,JS会为这个函数自动添加prototype属性,值是一个有constructor属性的对象,而不是空对象。当通过new关键进行调用此函数时,JS就会创建实例,并且通过设置此实例的proto指向构造函数的prototype来实现继承。

1
2
3
4
5
6
function Person() {
this.speak = function() {};
}
Person.prototype.run = function() {};
a = new Person();
console.log(a);

从打印的a实例中可知,speak方法是存在与a里的,而run方法是通过原型链(proto)访问。

构造函数的属性

1
2
3
4
5
6
7
8
9
10
function Person() {
var str = "string"; //构造函数里的私有变量
this.name = "xxx";
this.speak = function() { //能够访问构造函数里的私有变量
console.log(str);
}
}
p = new Person();
//p.str 无法访问
p.speak();

定义在构造函数内的方法,可以访问到函数内部用var定义的变量

constructor属性

1
2
3
4
function Person() {}
p = new Person();
console.log(p.constructor);
p2 = new p.constructor();

每个对象实例都有construction属性来指向他们的构造函数,当然也可以利用这种方式来创建新的实例。

原型式继承

1
2
3
4
5
6
7
8
9
10
function Person(name){
this.name = name
}
Person.prototype.speak = function() {console.log("speaking");};
function Teacher(tname){
Person.call(this,tname) // 调用父级构造器帮忙初始化继承的参数
}
Teacher.prototype = Object.create(Person.prototype);
Teacher.prototype.constructor = Teacher;
var teacher = Teacher('xxxx');

Object.create()可返回一个对象所需的proto(绝对不要直接修改proto)。
因为修改了prototype,所以之后还需改回prototype中的构造器,这样就完成了Teacher类继承Person类。

new的实质

1
var o = new Foo();

当执行上述代码时,等同于下面的:

1
2
3
var o = new Object();
o.__proto__ = Foo.prototype;
Foo.call(o);
1
o.method();

若调用了某个方法,会先检查o是否有这个方法,没有的话会查找Object.getPrototypeOf(o).method,如果还是没有就会继续Object.getPrototypeOf(Object.getPrototypeOf(o)).method,一直递归查找。

函数

call & apply & bind

call方法和apply方法都是Function.prototype中的方法,主要作用都是将某个对象作为调用函数的上下文,来执行目标函数。

1
2
3
4
5
6
7
8
9
10
var obj = {
name:"xxxx"
};
function func(age){
console.log(this.name+":"+age);
}
func.apply(obj,[10]);
func.call(obj,10);

两者的第一个参数都是执行函数的 上下文对象 ,两者的区别主要是参数列表的不同,apply要求传入数组,数组中为目标函数的参数列表。而call传入的参数如果有多个的话就逗号隔开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
name:"xxxx"
};
function func(age){
console.log(this.name+":"+age);
}
var newFunc = func.bind(obj,10);
newFunc();
// 或者如下传参亦可
var newFunc = func.bind(obj);
newFunc(10);

bind同样可以将执行函数绑定指定的this,不同的是bind返回的是一个函数而不像apply和call那样直接执行,bind是柯里化函数,可以用于延迟计算。

this

this可以理解为执行的上下文(一个对象,里面的属性局部作用域能访问到),并非函数代码中声明的位置,而执行的位置(箭头函数的this除外)。

默认绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function a(){
console.log(this.x);
}
function b(){
x = 20; // 等同于this.x = 20
a();
}
function c() {
var x = 30;
a();
}
x = 10;
a(); // 10
b(); // 20
c(); // 20

一般的this绑定在全局域中,除非调用点改变全局域的值,否则主调函数内的var同名变量也不能改变this取值
浏览器中在全局域定义var变量等同于全局变量(window),但在node环境中不加var关键字才能定义全局变量。

隐含绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(){
console.log(this.a);
}
var obj = {
a : 2,
foo: foo
}
a = 10;
obj.foo() // 2
foo(); // 10

此时this绑定在obj对象中。需要注意的是即使foo函数写在obj对象内,也不是完全属于obj的,像foo()不冠以obj环境照样读取全局的a。

隐含丢失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
x : 1,
bar : bar
};
function bar(){
console.log(this.x);
}
x = 20;
var f = obj.bar;
f(); // 20

以上情况会导致隐含丢失,看上去用的是obj的环境上下文,实际定义函数变量后在调用却是绑定全局的this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var obj = {
x : 3,
foo : foo
};
function foo(){
console.log(this.x);
}
x = 20;
function bar(func){
func();
}
bar(obj.foo); // 20

以回调的方式传入,this同样会隐含丢失。

明确绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
console.log( this.a );
}
var obj = {
a : 2
};
a = 10;
foo.call( obj ); // 2
setTimeout(obj.foo,0); // 浏览器中输出10 node中输出undefined

使用call,apply会让this明确绑定在指定的上下文。

硬绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
x : 3,
foo : foo
};
function foo(){
console.log(this.x);
}
var bindFunc = function() {
foo.call(obj);
}
x = 20;
bindFunc(); // 3
bindFunc.call(global); // 3
setTimeout(bindFunc,0); // 3

硬绑定即有一个函数,通过给定的上下文以及调用函数,用明确绑定将其绑死,无论外部如何,也无法改变参数函数(bindFunc中的foo)里绑定的this。

javascript中Function.Prototype.bind即是如此实现硬绑定。

new绑定

1
2
3
4
5
6
7
8
9
function foo(a) {
this.a = a;
this.b = function(){
console.log(this.a);
}
}
var bar = new foo( 2 );
bar.b()

new一般会返回新构建的对象,而这个对象被设置为函数调用的this,this在这个函数内部!

以上四种绑定中,new绑定 > 明确绑定 > 隐含绑定 > 默认绑定

箭头函数没有自己的this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
return function() {
console.log(this.id);
};
}
var f = foo.call({id: 1});
f.call({id: 2}); // 2
// test arrow function
function bar() {
return () => {
console.log(this.id);
};
}
var b = bar.call({id: 1});
b.call({id: 2}); // 1

箭头函数没有自己的this,箭头函数内的this总指向外层函数的this,所以对箭头函数进行明确绑定是无效的。

常用函数

数组

名称 使用例子 描述
Array.from Array.from({length:10},(v,i)=>{..}) 用于创建数组实例
Array.isArray() Array.isArray(L) 判断是否为数组对象
Array.prototype.concat() a1.concat(a2,a3,a4,…) 返回一个组合并了多个数组的对象
Array.prototype.every() a1.every(func(ele,index,arr){}) 根据回调函数返回的布尔检查每个元素是否通过测试
Array.prototype.fill() a1.fill(x) 将数组每一项用x来填充
Array.prototype.filter() a1.filter(func(ele,index,arr){}) 根据回调函数返回的布尔筛选数组每一项
Array.prototype.forEach() a1.forEach(func(ele,index,arr){}) 对每个元素都执行一次回调函数
Array.prototype.find() a.find(func(ele,index,arr){}) 根据回调函数返回的布尔值找出第一个符合条件的元素
Array.prototype.findIndex() 同上 跟find差不多,只是返回的是索引
Array.prototype.includes() a1.includes(x) 判断数组是否含有x元素
Array.prototype.indexOf() arr.indexOf(searchElement[, fromIndex = 0]) 找出第一个指定元素的索引
Array.prototype.map() a1.map(func(ele,index,arr){return ..}) 一个新数组,每个元素都是回调函数的结果
Array.prototype.push() a.push(1,2,3,…) 数组末尾推入元素
Array.prototype.pop() a.pop() 数组末尾删除元素,并且返回该元素
Array.prototype.reverse() a1.reverse() 返回一个原数组翻转的数组
Array.prototype.shift() a1.shift() 删除并返回第一个元素
Array.prototype.unshift() a1.unshift(x,2,3,…) 在数组开头依次添加元素
Array.prototype.sort() a1.sort([func(a,b){return 0/1/-1}]) 数组排序,会改变原数组
Array.prototype.splice() a1.splice(start[, deleteCount[, item1[, item2[, …]]]]) 删除或添加元素,若deletecount不指定,会从start删到结束,deleteCount=0表示添加元素
Array.prototype.slice() arr.slice([begin,end]); 返回元素的切片,前包后不包
Array.prototype.join() a1.join(separator) 根据连接符将元素连接成字符串

字符串

名称 使用例子 描述
String.prototype.concat() str.concat(string2, string3[, …, stringN]) 连接字符串
String.prototype.trim() str.trim() 清除字符串左右空格
String.prototype.toLowerCase() str.toLowerCase 转小写(toUpperCase为转大写)
String.prototype.match() str.match(regexp) 正则匹配字符串,返回数组,没有匹配则返回null
String.prototype.replace() str.replace(regexp或substr, newSubStr或function(oldStr)) 返回替换后的新的字符串,原数组不变
String.prototype.search() str.search(regexp) 寻找目的字符串,第一个寻找成功返回其索引,否则返回-1
String.prototype.slice() str.slice(beginSlice[, endSlice]) 字符串切片,返回一个从原字符串中提取出来的新字符串
String.prototype.split() str.split([separator[, limit]]) limit限定返回数量,将字符串按分隔符分割成数组
String.prototype.indexOf() str.indexOf(searchValue[, fromIndex]) 从寻找目的值第一次出现的索引,没有则返回-1
String.prototype.lastIndexOf() str.lastIndexOf(searchValue[, fromIndex]) 寻找目的值最后出现的索引,没有则返回-1
String.prototype.substr() str.substr(start[, length]) 返回一个字符串中从指定位置开始到指定字符数的字符
String.prototype.substring() str.substring(indexStart[, indexEnd]) 返回索引范围的子串,indexEnd不存在就返回到末尾,区间为前包后不包