标签模板字面量

标签模板字面量

该方法可以在字符串模板之前加入函数,用来处理模板:

function foo(str, ...val) {
    console.log(str);
    console.log(val);
}
var desc = "awesome";

foo`Everything is ${desc}!`;
// ["Everything is ", "!"]
// ["awesome"]
str为字符串数组,val为所有变量数组

该方法可以用来过滤HTML字符串,防止恶意攻击

let sender = '<script>alert("abc")</script>'; // 恶意代码
let message =
  SaferHTML`<p>${sender} has sent you a message.</p>`;

function SaferHTML(templateData) {
  let s = templateData[0];
  for (let i = 1; i < arguments.length; i++) {
    let arg = String(arguments[i]);

    // Escape special characters in the substitution.
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // Don't escape special characters in the template.
    s += templateData[i];
  }
  return s;
}

// <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>

sender为用户输入的变量,该方法可以过滤掉用户输入的HTML标签

实现国际化 参考

let I18n = {
  use({locale, defaultCurrency, messageBundle}) {
    I18n.locale = locale;
    I18n.defaultCurrency = defaultCurrency;
    I18n.messageBundle = messageBundle;
    return I18n.translate;
  },

  translate(strings, ...values) {
    let translationKey = I18n._buildKey(strings);
    let translationString = I18n.messageBundle[translationKey];

    if (translationString) {
      let typeInfoForValues = strings.slice(1).map(I18n._extractTypeInfo);
      let localizedValues = values.map((v, i) => I18n._localize(v, typeInfoForValues[i]));
      return I18n._buildMessage(translationString, ...localizedValues);
    }

    return 'Error: translation missing!';
  },
  _localizers: {
    s /*string*/: v => v.toLocaleString(I18n.locale),
    c /*currency*/: (v, currency) => (
      v.toLocaleString(I18n.locale, {
        style: 'currency',
        currency: currency || I18n.defaultCurrency
      })
    ),
    n /*number*/: (v, fractionalDigits) => (
      v.toLocaleString(I18n.locale, {
        minimumFractionDigits: fractionalDigits,
        maximumFractionDigits: fractionalDigits
      })
    )
  },

  _extractTypeInfo(str) {
    let match = typeInfoRegex.exec(str);
    if (match) {
      return {type: match[1], options: match[3]};
    } else {
      return {type: 's', options: ''};
    }
  },

  _localize(value, {type, options}) {
    return I18n._localizers[type](value, options);
  },

  // e.g. I18n._buildKey(['', ' has ', ':c in the']) == '{0} has {1} in the bank'
  _buildKey(strings) {
    let stripType = s => s.replace(typeInfoRegex, '');
    let lastPartialKey = stripType(strings[strings.length - 1]);
    let prependPartialKey = (memo, curr, i) => `${stripType(curr)}{${i}}${memo}`;

    return strings.slice(0, -1).reduceRight(prependPartialKey, lastPartialKey);
  },

  // e.g. I18n._formatStrings('{0} {1}!', 'hello', 'world') == 'hello world!'
  _buildMessage(str, ...values) {
    return str.replace(/{(\d)}/g, (_, index) => values[Number(index)]);
  }
};

let name = 'Bob';
let amount = 1234.56;
let i18n;

let messageBundle_fr = {
  'Hello {0}, you have {1} in your bank account.': 'Bonjour {0}, vous avez {1} dans votre compte bancaire.'
};
let messageBundle_de = {
  'Hello {0}, you have {1} in your bank account.': 'Hallo {0}, Sie haben {1} auf Ihrem Bankkonto'
};
let messageBundle_zh_Hant = {
  'Hello {0}, you have {1} in your bank account.': '你好{0},你有{1}在您的銀行帳戶。'
};

i18n = I18n.use({locale: 'fr-CA', defaultCurrency: 'CAD', messageBundle: messageBundle_fr});
i18n`Hello ${name}, you have ${amount}:c in your bank account.`;
// => 'Bonjour Bob, vous avez 1 234,56 $CA dans votre compte bancaire.'

i18n = I18n.use({locale: 'de-DE', defaultCurrency: 'EUR', messageBundle: messageBundle_de});
i18n`Hello ${name}, you have ${amount}:c in your bank account.`;
// => 'Hallo Bob, Sie haben 1.234,56 € auf Ihrem Bankkonto.'

i18n = I18n.use({locale: 'zh-Hant-CN', defaultCurrency: 'CNY', messageBundle: messageBundle_zh_Hant});
i18n`Hello ${name}, you have ${amount}:c in your bank account.`;
// => '你好Bob,你有¥1,234.56在您的銀行帳戶。'

除此之外,你甚至可以使用标签模板,在 JavaScript 语言之中嵌入其他语言jsx具体实现

jsx`
  <div>
    <input
      ref='input'
      onChange='${this.handleChange}'
      defaultValue='${this.state.value}' />
      ${this.state.value}
   </div>
`

String.raw()

raw方法返回一个斜杠都会被在前面添加一个斜杠,用来转译字符串。如果原字符串已经被转译,则会再次转译。

String.raw`Hi\n${2+3}!`;
// 返回 "Hi\\n5!"

String.raw`Hi\u000A!`;
// 返回 "Hi\\u000A!"

String.raw`Hi\\n`
// 返回 "Hi\\\\n"

在实际项目中原本准备用此方法,用来处理emoji的unicode编码,但是在{}内的变量含有\时无法被转译,最后通过escape多写了一层算法进行处理。

深度理解var、let、const及块级作用域

在ES5中变量作用域的基本单元是function,如果要创建一个块级作用域,则需要创建一个函数,或者使用立即调用函数表达式(IIFE)。而对于var定义的变量的作用域,作用在一个函数块中,或者存在于整个全局中(全局变量)。在ES6中出现了let和const两个变量定义方法:

let声明

与使用var声明的变量总是在函数作用域中不同,使用let声明时可以只用{…}就能创建一个作用域。

var a = 2;
{
    let a = 3;
    console.log(a);  // 3
}
console.log(a);      // 2

在let声明之前访问该变量会导致报错,这称为临时死亡区(Temporal Dead Zone, TDZ)错误。而使用var的话这个顺序是无关紧要的。

console.log(a); // undefined
console.log(b); // ReferenceError!

var a;
let b;

在for循环中使用let,不只是声明了一个变量,而是循环的每一次迭代都重新声明了一个新的变量,这点是与var不同的。

var funcs = [];
for (let i = 0;i < 5;i++) {
    funcs.push(function({
        console.log(i);
    })
}
funcs[3](); // 3
而如果把let换成var的话,打印出来的值为5

const声明

const声明用于定义常量,常量是一个设定了初始值之后就只读的变量。但是常量不是对这个值的本身进行限制,而是对赋值的那个变量限制。如果这个值时复杂值,比如对象或者数组,这个内容是可以修改的。

const a = [1,2,3];
a.push(4);
console.log(a);     // [1,2,3,4]
a = 42;             // TypeError!

块作用域函数

在ES6之前,函数调用没有块的概念,在内部定义的函数在外部也可以直接调用,而在ES6之后,块内定义的函数,其作用域就在这个块内,外部调用时会报错。函数调用支持‘提升’,即不存在TDZ错误。

File对象

Blob

Blob(Binary Large Object)对象代表了一段二进制数据,提供了一系列操作接口。其他操作二进制数据的API(比如File对象),都是建立在Blob对象基础上的,继承了它的属性和方法。

生成Blob对象有两种方法:一种是使用Blob构造函数,另一种是对现有的Blob对象使用slice方法切出一部分

FileList

FileList对象针对表单的file控件。当用户通过file控件选取文件后,这个控件的files属性值就是FileList对象。它在结构上类似于数组,包含用户选取的多个文件。

<input type="file" id="input" onchange="console.log(this.files.length)" multiple />

采用拖放方式,也可以得到FileList对象。

var dropZone = document.getElementById('drop_zone');
dropZone.addEventListener('drop', handleFileSelect, false);

function handleFileSelect(evt) {
    evt.stopPropagation();
    evt.preventDefault();

    var files = evt.dataTransfer.files; // FileList object.

    // ...
}

字符串常用方法总结

length属性

length算是字符串中非常常用的一个属性了,它的功能是获取字符串的长度。当然需要注意的是js中的中文每个汉字也只代表一个字符,这里可能跟其他语言有些不一样。

var str = 'abc';
console.log(str.length);

prototype属性

prototype在面向对象编程中会经常用到,用来给对象添加属性或方法,并且添加的方法或属性在所有的实例上共享。因此也常用来扩展js内置对象,如下面的代码给字符串添加了一个去除两边空格的方法:

String.prototype.trim = function(){
    return this.replace(/^\s*|\s*$/g, '');
}

charAt()

charAt()方法可用来获取指定位置的字符串,index为字符串索引值,从0开始到string.leng - 1,若不在这个范围将返回一个空字符串。如:

var str = 'abcde';
console.log(str.charAt(2)); 
//返回c
console.log(str.charAt(8)); 
//返回空字符串

charCodeAt()

charCodeAt()方法可返回指定位置的字符的Unicode编码。charCodeAt()方法与charAt()方法类似,都需要传入一个索引值作为参数,区别是前者返回指定位置的字符的编码,而后者返回的是字符子串。

var str = 'abcde';
console.log(str.charCodeAt(0)); 
//返回97

fromCharCode()

fromCharCode()可接受一个或多个Unicode值,然后返回一个字符串。另外该方法是String 的静态方法,字符串中的每个字符都由单独的数字Unicode编码指定。

String.fromCharCode(97, 98, 99, 100, 101) 
//返回abcde

indexOf()

indexOf()用来检索指定的字符串值在字符串中首次出现的位置。它可以接收两个参数,searchvalue表示要查找的子字符串,fromindex表示查找的开始位置,省略的话则从开始位置进行检索。

var str = 'abcdeabcde';
console.log(str.indexOf('a'));    
// 返回0
console.log(str.indexOf('a', 3));    
// 返回5
console.log(str.indexOf('bc'));    
// 返回1

lastIndexOf()

lastIndexOf()语法与indexOf()类似,它返回的是一个指定的子字符串值最后出现的位置,其检索顺序是从后向前。

var str = 'abcdeabcde';
console.log(str.lastIndexOf('a'));    
// 返回5
console.log(str.lastIndexOf('a', 3));    
// 返回0
从第索引3的位置往前检索
console.log(str.lastIndexOf('bc'));    
// 返回6

search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。它会返回第一个匹配的子字符串的起始位置,如果没有匹配的,则返回-1。

var str = 'abcDEF';
console.log(str.search('c'));    
//返回2
console.log(str.search('d'));    
//返回-1
console.log(str.search(/d/i));    
//返回3

match()

match()方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。

如果参数中传入的是子字符串或是没有进行全局匹配的正则表达式,那么match()方法会从开始位置执行一次匹配,如果没有匹配到结果,则返回null。否则则会返回一个数组,该数组的第0个元素存放的是匹配文本,除此之外,返回的数组还含有两个对象属性index和input,分别表示匹配文本的起始字符索引和stringObject 的引用(即原字符串)。

var str = '1a2b3c4d5e';
console.log(str.match('h'));    
//返回null
console.log(str.match('b'));    
//返回["b", index: 3, input: "1a2b3c4d5e"]
console.log(str.match(/b/));    
//返回["b", index: 3, input: "1a2b3c4d5e"]

如果参数传入的是具有全局匹配的正则表达式,那么match()从开始位置进行多次匹配,直到最后。如果没有匹配到结果,则返回null。否则则会返回一个数组,数组中存放所有符合要求的子字符串,并且没有index和input属性。

var str = '1a2b3c4d5e';
console.log(str.match(/h/g));    
//返回null
console.log(str.match(/\d/g));    
//返回["1", "2", "3", "4", "5"]

substring()

substring()是最常用到的字符串截取方法,它可以接收两个参数(参数不能为负值),分别是要截取的开始位置和结束位置,它将返回一个新的字符串,其内容是从start处到end-1处的所有字符。若结束参数(end)省略,则表示从start位置一直截取到最后。

var str = 'abcdefg';
console.log(str.substring(1, 4));    
//返回bcd
console.log(str.substring(1));    
//返回bcdefg
console.log(str.substring(-1));    
//返回abcdefg,传入负值时会视为0

slice()

slice()方法与substring()方法非常类似,它传入的两个参数也分别对应着开始位置和结束位置。而区别在于,slice()中的参数可以为负值,如果参数是负数,则该参数规定的是从字符串的尾部开始算起的位置。也就是说,-1 指字符串的最后一个字符。

var str = 'abcdefg';
console.log(str.slice(1, 4));    
//返回bcd
console.log(str.slice(-3, -1));    
//返回ef
console.log(str.slice(1, -1));    
//返回bcdef
console.log(str.slice(-1, -3));    
//返回空字符串,若传入的参数有问题,则返回空

substr()

substr()方法可在字符串中抽取从start下标开始的指定数目的字符。其返回值为一个字符串,包含从 stringObject的start(包括start所指的字符)处开始的length个字符。如果没有指定 length,那么返回的字符串包含从start到stringObject的结尾的字符。另外如果start为负数,则表示从字符串尾部开始算起。

var str = 'abcdefg';
console.log(str.substr(1, 3))    
//返回bcd
console.log(str.substr(2))    
//返回cdefg
console.log(str.substr(-2, 4))    
//返回fg,目标长度较大的话,以实际截取的长度为准

replace()

replace()方法用来进行字符串替换操作,它可以接收两个参数,前者为被替换的子字符串(可以是正则),后者为用来替换的文本。

如果第一个参数传入的是子字符串或是没有进行全局匹配的正则表达式,那么replace()方法将只进行一次替换(即替换最前面的),返回经过一次替换后的结果字符串。

var str = 'abcdeabcde';
console.log(str.replace('a', 'A'));
console.log(str.replace(/a/, 'A'));

如果第一个参数传入的全局匹配的正则表达式,那么replace()将会对符合条件的子字符串进行多次替换,最后返回经过多次替换的结果字符串。

var str = 'abcdeabcdeABCDE';
console.log(str.replace(/a/g, 'A'));    
//返回AbcdeAbcdeABCDE
console.log(str.replace(/a/gi, '$'));    
//返回$bcde$bcde$BCDE

split()

split()方法用于把一个字符串分割成字符串数组。第一个参数separator表示分割位置(参考符),第二个参数howmany表示返回数组的允许最大长度(一般情况下不设置)。

var str = 'a|b|c|d|e';
console.log(str.split('|'));    
//返回["a", "b", "c", "d", "e"]
console.log(str.split('|', 3));    
//返回["a", "b", "c"]
console.log(str.split(''));    
//返回["a", "|", "b", "|", "c", "|", "d", "|", "e"]

也可以用正则来进行分割

var str = 'a1b2c3d4e';
console.log(str.split(/\d/)); 
//返回["a", "b", "c", "d", "e"]

toLowerCase()和toUpperCase()

toLowerCase()方法可以把字符串中的大写字母转换为小写,toUpperCase()方法可以把字符串中的小写字母转换为大写。

var str = 'JavaScript';
console.log(str.toLowerCase());    
//返回javascript
console.log(str.toUpperCase());    
//返回JAVASCRIPT

水平居中和垂直居中方法总结

基本方法

1.已知宽度块元素宽度

.child{width:1000px;margin:0 auto;}

2.文本内容居中

.parent{text-align:center;}

3.文本垂直居中

.child{height:20px;line-height:20px}

综合使用

1.已知尺寸,设置position: absolute方法

.parent {position:relative;}
.child {
    position:absolute;
    top:50%;
    left:50%;
    width:150px;
    height:150px;
    margin-left:-75px;
    margin-top:-75px;
}

2.未知尺寸,设置position: absolute方法

.parent{position:relative;}
.child{
    position:absolute;
    top:50%;
    left:50%;
    -webkit-transform:translateX(-50%);
    transform:translateX(-50%);
    -webkit-transform:translateY(-50%);
    transform:translateY(-50%);
}

3.设置position: absolute方法,各方向均设置为0

content { 
    position:absolute; 
    top:0; 
    bottom:0;
    left:0; 
    right:0; 
    margin:auto; 
    height:240px; 
    width:70%; 
}

4.display: table-cell属性

.parent {
  height: 100px;
  width: 100px;
  background: #eee;
  display: table-cell;
  text-align: center;
  vertical-align: middle;
}
.child {
  color: red;
  background: red;
  display: inline-block;
}

5.display: flex属性

.parent{
  display:flex;
  justify-content:center;
  align-items:center;
}

new操作符

实现new操作符的原理

function A (x) {
    this.x = x;
}

function newA (x) {
    var o = {};
    o.__proto__ = A.prototype; // 原型继承
    A.call(o, x);
    return o;
}
var a = newA(1); // 该方法与var a = new A(1)相同
console.log(a.x); // 1

理解proto和prototype

在js中每个对象一定对应一个原型对象(proto),并从原型对象继承属性和方法。

a.__proto__ === A.prototype

不像每个对象都有proto属性来标识自己所继承的原型,只有函数才有prototype属性。当你创建函数时,JS会为这个函数自动添加prototype属性,值是空对象。而一旦你把这个函数当作构造函数(constructor)调用(即通过new关键字调用),那么JS就会帮你创建该构造函数的实例,实例继承构造函数prototype的所有属性和方法(实例通过设置自己的proto指向承构造函数的prototype来实现这种继承)。

this详解

js中this的调用方式主要有以下几种。

普通函数调用

在这种情况下,属于全局性的调用,this就代表全局对象window。

// this指向的就是全局window,该方式相当于定义一个全局变量
function a () {
    this.x = 1
    console.log(this.x)
}
a(); // 1

// 将变量移到外面,调用结果一样
var x = 1
function a () {
    console.log(this.x)
}
a(); // 1

作为方法调用

该种方法通过对象方法调用,这时this指向这个对象的上级,且具体指向由调用时来决定。

// 这里的this指向该对象,所以输出2
var x = 0;
var a = {
    x: 1;
    showX: function () {
        console.log(this.name);
    }
}
a.showX(); // 1

// 这里将a.showX赋值给全局变量,所以此时的this指向window
var m = a.showX;
m(); // 0

//虽然showX方法在a中定义,但是调用时候却是在b中调用,因此this指向b
var a = {
    x: 1,
    showX: function () {
        console.log(this.x);
    }
}
var b = {
    x: 2,
    showX: a.showX
}
b.showX(); // 2

作为构造函数调用

function A (x) {
    this.x = x;
}
var a = new A();
a.x(1); // 1

call/apply方法调用

使用call/apply方法可以改变this的指向

var x = 0;
var a = {
    x: 1,
    showX: function () {
        console.log(this.name);
    }
}
a.showX.call(); // 0

bind方法调用

setTimeout/setInterval/匿名函数执行的时候,this默认指向window对象,除非手动改变this的指向。在《javascript高级程序设计》当中,写到:“超时调用的代码(setTimeout)都是在全局作用域中执行的,因此函数中的this的值,在非严格模式下是指向window对象,在严格模式下是指向undefined”

// 这里的setTimeout函数,相当于window.setTimeout(),此时this指向全局
var x = 0;
function A (x) {
    this.x = x;
    this.showX = function () {
        setTimeout(function () {
            console.log(this.x);
        }, 50)
    }
}
var a = new A(1);
a.showX(); // 0

// 通过bind()方法,绑定setTimeout里面的匿名函数this一直指向A
var x = 0;
function A (x) {
    this.x = x;
    this.showX = function () {
        setTimeout(function () {
            console.log(this.x);
        }.bind(this), 50)
    }
}
var a = new A(1);
a.showX(); // 1

箭头函数

es6里面this指向固定化,始终指向外部对象,因为箭头函数没有this,因此它自身不能进行new实例化,同时也不能使用call, apply, bind等方法来改变this的指向

   function Timer() {
        this.seconds = 0;
        setInterval( () => this.seconds ++, 1000);
    } 

    var timer = new Timer();

    setTimeout( () => console.log(timer.seconds), 3100);

    // 3

    在构造函数内部的setInterval()内的回调函数,this始终指向实例化的对象,并获取实例化对象的seconds的属性,每1s这个属性的值都会增加1。否则最后在3s后执行setTimeOut()函数执行后输出的是0

ES7新特性

Array.prototype.includes

该方法用来检测一个数组中是否包含某个元素,之前使用indexOf来实现,但是它只能返回元素在数组中的位置或者-1,而使用该方法可以返回Boolean值来进行判断。

基本使用

在ES7中,可以使用includes新特性来直接判断数组中是否含有某个元素,includes()方法可以返回布尔值。

let arr = ['a', 'b', 'c'];

arr.includes('b'); // true
arr.includes('d'); // false

在字符串中使用

let words = 'abc';

words.includes('ab'); // true
words.includes('d'); // false

幂运算

是指乘方运算的结果。2的3次幂相当于2 2 2 = 8。幂运算大多数是为了开发一些数学计算,对于3D,VR,SVG还有数据可视化非常有用。之前的话可以自定义递归函数来实现,或者通过Math.pow()方法。在ES7中,幂运算已经被集成到了运算符中,该符号为:**。

let a = 7 ** 12
let b = 2 ** 7
console.log(a === Math.pow(7,12)) // true
console.log(b === Math.pow(2,7)) // true

let a = 7
a **= 12
let b = 2
b **= 7
console.log(a === Math.pow(7,12)) // true
console.log(b === Math.pow(2,7)) // true

跨域详细梳理

什么是跨域

跨域一词从字面意思看,就是跨域名嘛,但实际上跨域的范围绝对不止那么狭隘。具体概念如下:只要协议、域名、端口有任何一个不同,都被当作是不同的域。之所以会产生跨域这个问题呢,其实也很容易想明白,要是随便引用外部文件,不同标签下的页面引用类似的彼此的文件,浏览器很容易懵逼的,安全也得不到保障了就。什么事,都是安全第一嘛。但在安全限制的同时也给注入iframe或是ajax应用上带来了不少麻烦。所以我们要通过一些方法使本域的js能够操作其他域的页面对象或者使其他域的js能操作本域的页面对象(iframe之间)。下面是具体的跨域情况详解:

URL                      说明       是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js     同一域名下   允许

http://www.a.com/lab/a.js
http://www.a.com/script/b.js 同一域名下不同文件夹 允许

http://www.a.com:8000/a.js
http://www.a.com/b.js     同一域名,不同端口  不允许

http://www.a.com/a.js
https://www.a.com/b.js 同一域名,不同协议 不允许

http://www.a.com/a.js
http://70.32.92.74/b.js 域名和域名对应ip 不允许

http://www.a.com/a.js
http://script.a.com/b.js 主域相同,子域不同 不允许(cookie这种情况下也不允许访问)

http://www.a.com/a.js
http://a.com/b.js 同一域名,不同二级域名(同上) 不允许(cookie这种情况下也不允许访问)

http://www.cnblogs.com/a.js
http://www.a.com/b.js 不同域名 不允许

有src的标签可实现跨域

所有具有src属性的HTML标签都是可以跨域的,包括, ,该方法需要创建一个DOM对象,且只能用于GET方法。

在document.body中append一个具有src属性的HTML标签,src属性值指向的URL会以GET方法被访问,该访问是可以跨域的。其实样式表的标签也是可以跨域的,只要是有src或href的HTML标签都有跨域的能力。

不同的HTML标签发送HTTP请求的时机不同,例如在更改src属性时就会发送请求,而script, iframe, link[rel=stylesheet]只有在添加到DOM树之后才会发送HTTP请求:

var img = new Image();
img.src = 'http://some/picture';        // 发送HTTP请求

var ifr = $('<iframe>', {src: 'http://b.a.com/bar'});
$('body').append(ifr);                  // 发送HTTP请求

设置document.domain跨域

相同主域名不同子域名下的页面,可以设置document.domain让它们同域,同域document提供的是页面间的互操作,需要载入iframe页面。

// 在www.a.com/a.html中
document.domain = 'a.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://www.script.a.com/b.html';
ifr.display = none;
document.body.appendChild(ifr);
ifr.onload = function(){
    var doc = ifr.contentDocument || ifr.contentWindow.document;
    //在这里操作doc,也就是b.html
    ifr.onload = null;
};
// 在www.script.a.com/b.html中
document.domain = 'a.com';

使用jsonp进行跨域

jsonp = json(json数据) + padding(填充)
其实对于常用性来说,jsonp应该是使用最经常的一种跨域方式了,他不受浏览器兼容性的限制。但是他也有他的局限性,只能发送 GET 请求,需要服务端和前端规定好,写法丑陋。它的原理在于浏览器请求 script 资源不受同源策略限制,并且请求到 script 资源后立即执行。

浏览器端

首先全局注册一个callback回调函数,记住这个函数名字(比如:resolveJson),这个函数接受一个参数,参数是期望的到的服务端返回数据,函数的具体内容是处理这个数据。然后动态生成一个 script 标签,src 为:请求资源的地址+获取函数的字段名+回调函数名称,这里的获取函数的字段名是要和服务端约定好的,是为了让服务端拿到回调函数名称。(如:www.qiute.com?callbackName=resolveJson)。

function resolveJosn(result) {
    console.log(result.name);
}
var jsonpScript= document.createElement("script");
jsonpScript.type = "text/javascript";
jsonpScript.src = "https://www.qiute.com?callbackName=resolveJson";
document.getElementsByTagName("head")[0].appendChild(jsonpScript);

服务端

在接受到浏览器端 script 的请求之后,从url的query的callbackName获取到回调函数的名字,例子中是resolveJson。然后动态生成一段javascript片段去给这个函数传入参数执行这个函数。比如:

resolveJson({name: 'qiutc'});

执行

服务端返回这个 script 之后,浏览器端获取到 script 资源,然后会立即执行这个 javascript,也就是上面那个片段。这样就能根据之前写好的回调函数处理这些数据了。

window.name跨域

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。比如有一个www.qiutc.me/a.html页面,需要通过a.html页面里的js来获取另一个位于不同域上的页面www.qiutc.com/data.html里的数据。data.html页面里的代码很简单,就是给当前的window.name设置一个a.html页面想要得到的数据值。data.html里的代码:

<script>
window.name = '我是被期望得到的数据';
</script>

那么在 a.html 页面中,我们怎么把 data.html 页面载入进来呢?显然我们不能直接在 a.html 页面中通过改变 window.location 来载入data.html页面(这简直扯蛋)因为我们想要即使 a.html页面不跳转也能得到 data.html 里的数据。答案就是在 a.html 页面中使用一个隐藏的 iframe 来充当一个中间人角色,由 iframe 去获取 data.html 的数据,然后 a.html 再去得到 iframe 获取到的数据。充当中间人的 iframe 想要获取到data.html的通过window.name设置的数据,只需要把这个iframe的src设为www.qiutc.com/data.html就行了。然后a.html想要得到iframe所获取到的数据,也就是想要得到iframe的window.name的值,还必须把这个iframe的src设成跟a.html页面同一个域才行,不然根据前面讲的同源策略,a.html是不能访问到iframe里的window.name属性的。这就是整个跨域过程。

// a.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script>
    function getData() {
        var iframe =document.getElementById('iframe');
        iframe.onload = function() {
            var data = iframe.contentWindow.name; // 得到
        }
        iframe.src = 'b.html';  // 这里b和a同源
    }
  </script>
</head>
<body>
  <iframe src="https://www.qiutc.com/data.html" style="display:none" onload="getData()"</iframe>
</body>
</html>

window.postMessage跨域

HTML5允许窗口之间发送消息,浏览器需要支持HTML5,获取窗口句柄后才能相互通信。这是一个安全的跨域通信方法,postMessage(message,targetOrigin)也是HTML5引入的特性。可以给任何一个window发送消息,不论是否同源。第二个参数可以是*但如果你设置了一个URL但不相符,那么该事件不会被分发。看一个普通的使用方式吧:

// URL: http://a.com/foo
var win = window.open('http://b.com/bar');
win.postMessage('Hello, bar!', 'http://b.com');
// URL: http://b.com/bar
window.addEventListener('message',function(event) {
    console.log(event.data);
});

跨域资源共享(CORS)

服务器设置Access-Control-Allow-OriginHTTP响应头之后,浏览器将会允许跨域请求,浏览器需要支持HTML5,可以支持POST,PUT等方法。前面提到的跨域手段都是某种意义上的Hack, HTML5标准中提出的跨域资源共享(Cross Origin Resource Share,CORS)才是正道。 它支持其他的HTTP方法如PUT, POST等,可以从本质上解决跨域问题。

例如,从http://a.com要访问http://b.com的数据,通常情况下Chrome会因跨域请求而报错:

XMLHttpRequest cannot load http://b.com. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://a.com' is therefore not allowed access.

错误原因是被请求资源没有设置Access-Control-Allow-Origin,所以我们在b.com的服务器中设置这个响应头字段即可:

Access-Control-Allow-Origin: *              # 允许所有域名访问,或者
Access-Control-Allow-Origin: http://a.com   # 只允许所有域名访问